= 5.1.15 — 2026-08-21 =
* fix: On the order-received page, reopening the payment modal from the "Got it, thank you!" sticky bar's View button (after a manual M-Pesa code had already been submitted) silently reloaded the whole page about two seconds later. The reopen path reused the same state transition as a fresh code submission, which always schedules an auto-redirect back to the order page — appropriate right after submitting, not when a customer just wants to look at the status again. Reopening from the sticky bar no longer schedules that redirect.

= 5.1.14 — 2026-08-21 =
* security: The previous release stopped Free's callback handlers from *executing* Pro's deposit/balance confirmation logic, but left the now-fully-orphaned methods themselves in class-order-meta.php: set_deposit_data(), confirm_deposit(), mark_balance_collected(), set_refund_data(), confirm_refund() — all fully-implemented, callable, state-changing Pro business logic with zero callers anywhere in Free (not even in tests). A callable method nothing calls is still shipping the feature "just in case", which is the exact pattern the original WordPress.org review warned about. Removed all five; kept only the read-only getters with real production callers (displaying legacy order data). get_all() (used by the order panel) rewritten to read the remaining raw meta directly instead of through the removed wrapper getters.
* change: Reworded class-ozupay-features.php's docblocks, which described the class in terms of "Pro-only feature keys" even though it gates nothing — wording alone can invite the same trialware scrutiny as the real thing. Now describes it plainly as a compatibility shim for one shared call site (class-payment-waiting.php), which is all it actually is in Free.

= 5.1.13 — 2026-08-21 =
* security: A legacy M-Pesa on Delivery (COD deposit) order left over from a Pro-to-Free downgrade could have Free's STK/C2B/manual-verification callback handlers automatically run Pro's deposit/balance confirmation logic on a real incoming payment — including transitioning the order to "awaiting-cod", a status Free doesn't even register (class-order-statuses.php is Pro-only), leaving the order in a broken, unmanageable state. Free never had the checkout UI or settings to create such an order itself, only to receive a callback for one. Replaced the automated deposit/balance confirmation (underpayment math, status transitions, payment_complete()) with a safe record-and-flag: the payment is still logged to the transaction table exactly as before (nothing is ever silently lost) and a clear order note explains what happened, but Free no longer pretends to run business logic it doesn't have. Free-only change — Pro's real deposit/balance confirmation is untouched.

= 5.1.12 — 2026-08-21 =
* fix: Telemetry's error-classification and metrics-counter hooks were registered unconditionally, so local usage counters (class-metrics-buffer.php) accumulated even while telemetry was disabled. Nothing was ever transmitted without consent — scheduling and sending were already gated — but the local accumulation itself wasn't. Hook registration is now gated the same way, and locally buffered data is cleared on opt-out.

= 5.1.11 — 2026-08-21 =
* fix: The personal-data exporter skipped a manually-submitted M-Pesa code, and the privacy-policy suggested text said "related order data can be exported or erased" when the eraser only ever anonymized the phone number. Added the manual code to the exporter and rewrote the disclosure to accurately state that receipt number, amount paid, and manual code are retained (not erased) for accounting purposes.
* fix: Telemetry's daily cron event was scheduled unconditionally on every page load, regardless of whether diagnostic telemetry had been opted into — transmission itself was already correctly gated, but the schedule wasn't. The consent check now lives inside the scheduling function itself.
* fix: Currency, SSL, and callback-token admin notices were shown to any user with wp-admin access rather than only ones who can act on them. Scoped to the appropriate capability (manage_woocommerce or activate_plugins).

= 5.1.10 — 2026-08-21 =
* security: WordPress.org's Plugin Check flagged unescaped table-name interpolation ($table built from $wpdb->prefix, used directly in raw SQL) across several database-maintenance queries. Not exploitable in practice (never derived from user input), but every occurrence now passes through esc_sql() at construction time, matching the pattern already used elsewhere in this codebase.
* fix: readme.txt's "Tested up to" header was one WordPress version behind current stable, which excludes the plugin from directory search results.

= 5.1.9 — 2026-08-20 =
* change: Narrowed a broad PHPCS output-escaping suppression in the payment-waiting template (templates/checkout/payment-waiting.php) to the single line PHPCS actually flagged once the blanket disable was removed — an int cast on a retry-count printf argument. Manual review found no exploitable output under the old blanket suppression; this is a maintainability improvement so a future genuine escaping regression in this file can't hide behind it.

= 5.1.8 — 2026-08-20 =
* security: The 5.1.7 erasure fix only anonymized the transaction log's phone column when order metadata still had a phone to derive the anonymized value from — a partial/manual cleanup that had already cleared order meta could leave a real phone number behind in the transaction log. The column is now always erased, independent of order meta's current state.

= 5.1.7 — 2026-08-20 =
* security: A personal-data erasure request anonymized the phone number stored in order metadata but left it untouched in the dedicated phone column of the transaction log table, so the number wasn't actually removed. Erasure now anonymizes both.

= 5.1.6 — 2026-08-20 =
* change: Phone numbers in the Transaction Log admin table are now shown in full instead of partially masked (e.g. "254715XXXX35"), with a click-to-copy icon next to them.

= 5.1.5 — 2026-08-20 =
* fix: Removed two feature bullets from the readme ("Failure Notifications", "Advanced Admin Tools") that named specific Pro-only upsells rather than describing free-edition value. Also removed a dead, unreachable QR-code code path left over from Pro, and corrected code comments that incorrectly described manual M-Pesa transaction-code submission as a Pro-only feature — it's available in this edition via the dedicated Payment Issue page.

= 5.1.4 — 2026-08-20 =
* fix: The M-Pesa waiting screen's heading and "Sent to [phone] — enter your PIN" instruction used to stay on screen unchanged even after payment succeeded, failed, or timed out — sitting directly above a status message that said the opposite. The heading now updates to match the outcome (and reverts to the original instruction on Retry).

= 5.1.3 — 2026-08-20 =
* fix: The "Payment could not be confirmed" countdown on the M-Pesa waiting screen no longer keeps ticking down after a definitive failure (cancelled prompt, wrong PIN, expired prompt, insufficient funds) — it now stops immediately. It still keeps checking quietly in the background for the one ambiguous case (no response from the phone), so a payment that completes moments later is still picked up.

= 5.1.2 — 2026-08-17 =
* fix: The M-Pesa icon on WooCommerce → Payments and checkout was rendering at its full 128x128 source size instead of a small icon — now capped to 24x24px in both the classic and Blocks checkout.

= 5.1.1 — 2026-08-17 =
* change: M-Pesa now shows the OzuPay icon on WooCommerce → Settings → Payments and at checkout, instead of no icon at all.

= 5.1.0 — 2026-08-17 =
* feat: Added a "Checkout Settings" tab on OzuPay → Settings, showing the same Title/Description/Phone Label/Manual Instructions/STK Retries fields as WooCommerce → Settings → Payments → M-Pesa → Manage. Both screens read and write the same settings, so editing either one keeps them identical.
* fix: The floating Save Settings button on OzuPay → Settings now shows on the Checkout Settings tab too, and correctly submits that tab's own form instead of only working on the Credentials/Features/Plugin Data tabs.

= 5.0.14 — 2026-08-17 =
* fix: Plugin URI header no longer points at a WordPress.org URL (disallowed by the Plugin Directory guidelines) — now points at a dedicated ozupay.com page distinct from the Pro edition's.

= 5.0.13 — 2026-08-17 =
* fix: The created_at_utc backfill (added in 5.0.12) no longer retries indefinitely on very old rows with an invalid, pre-existing "0000-00-00 00:00:00" timestamp — those rows now correctly fall back to displaying their legacy timestamp instead of blocking the migration from completing.

= 5.0.12 — 2026-08-17 =
* fix: The "Date" column on the Transaction Log now stays correct after you change Settings → General → Timezone — previously, only the UTC offset label updated for existing rows, while the displayed time itself stayed frozen at whatever the timezone was when the row was recorded.

= 5.0.11 — 2026-08-17 =
* fix: Scheduled data-retention cleanup now uses the site's configured timezone instead of the database server's clock, so records are purged on the intended schedule regardless of server location.

= 5.0.10 — 2026-08-16 =
* change: Unified the Dashboard, Transaction Log, Settings, and About screens around one responsive OzuPay admin design system with consistent page headers, cards, controls, tables, spacing, and mobile behavior.

= 5.0.9 — 2026-08-14 =
* fix: Updated the plugin-row Upgrade to Pro action, About-page Get Pro button, and readme upgrade link to the current pricing section at `https://ozupay.com/#pricing`.

= 5.0.8 — 2026-08-14 =
* change: Styled the scoped Upgrade to Pro action on the Plugins screen in OzuPay green, with an accessible darker hover and keyboard-focus state.

= 5.0.7 — 2026-08-14 =
* privacy: Fixed telemetry consent parsing so the saved string `no` is disabled. Only an explicit `yes`, `1`, or boolean true opt-in enables reports; opting in schedules reporting and opting out removes its cron and pending activation report.
* change: Renamed the header-bearing main file to `ozupay-payment-gateway.php` to match the intended WordPress.org folder and slug. The old `ozupay.php` remains as a headerless compatibility bootstrap and migrates both normal and multisite activation records before loading the canonical file, preventing deactivation on upgrade.
* fix: Corrected stale Settings, About, Documentation, and Transaction Log admin URLs to their registered slugs.
* change: Moved the schema-version marker from `ozupay_db_version` to the distinctive `ozupay_mpesa_db_version` key. Existing values migrate before upgrade checks and the legacy key is deleted only after the new value is readable.
* docs: Assigned a unique WordPress.org Plugin URI, disclosed required and optional external services, removed undeployed screenshot declarations, and reduced `readme.txt` below the directory's 10 KB guidance.
* change: The plugin-row author link now points to ozupay.com, and a scoped "Upgrade to Pro" action links to the pricing page.

= 5.0.6 — 2026-08-14 =
* security: M-Pesa Paybill/Buy Goods payments can no longer confirm an order by guessing its order number when that number happens to match the order's internal ID. This closes a gap where a numeric Paybill reference could resolve straight to an order by ID with no further check that actually stopped it, letting a payment intended for one order be applied to a different customer's order. A reference that doesn't match your store's own order numbering is now treated as unrecognised, same as before this change was introduced.
* fix: A duplicate M-Pesa Paybill notification for a COD deposit order that's already awaiting its balance payment no longer triggers a false "unmatched payment" log/webhook entry.

= 5.0.5 — 2026-08-14 =
* fix: C2B URL registration status detection now recognizes Safaricom's actual success response. The registerurl endpoint returns a zero-padded response code ("00000000") rather than the plain "0" other Daraja endpoints use, so a genuinely successful registration was going unrecorded. (Registration itself is Pro-only; this affects the shared status-detection code both editions carry.)

= 5.0.4 — 2026-08-14 =
* fix: The C2B (Paybill fallback) callback URLs shown in Settings/Sandbox Testing for pasting into your Daraja app now use the original `ozupay_*` prefix again. The 5.0.0 rename to `ozupay_mpesa_*` broke registration on Safaricom's side, which rejects any URL containing "mpesa" with a "Bad Request" keyword error. Both prefixes keep working, so no action is needed beyond re-entering the URL shown now if you already registered the mpesa-prefixed one.

= 5.0.3 — 2026-08-14 =
* fix: Completes the client/server side of the distinctive-prefix rename. Settings fields now post under the namespace the save handler reads, Settings and Sandbox Testing send the nonce fields their handlers validate, and guarded shared-code class references use the renamed classes. Structural contract tests now cover these producer/consumer boundaries.
* fix: Migrates legacy `_ozupay_*` order metadata from WooCommerce HPOS storage as well as classic post metadata. The migration is independently gated and retryable so sites that already ran the original prefix migration still recover historical payment state safely.

= 5.0.2 — 2026-08-14 =
* feat: The OzuPay M-Pesa order panel now always shows the payment state as Unpaid, Partially paid, Paid, Partially refunded, or Refunded, based on the amounts actually received and returned.

= 5.0.1 — 2026-08-14 =
* fix: The table-prefix migration added in 5.0.0 could mark itself complete even when copying historical data into a new table had failed or been skipped, permanently stranding transaction history in the old table with no retry. It now only marks itself done once the row count is verified to match; anything short retries automatically on the next request.
* fix: A site downgraded from Pro could have M-Pesa on Delivery orders recorded under either of Pro's two gateway ID generations. The compatibility entry that keeps those orders' payment method readable now covers both IDs instead of just the original one.

= 5.0.0 — 2026-08-14 =
* change: Completes the WordPress.org reviewer's distinctive-prefix requirement started in 4.0.0 (PPR4-01). Both custom database tables (transactions, locks) and the STK/status/manual-verification Daraja callback routes now use the `ozupay_mpesa_*` prefix. Existing transaction history migrates automatically on upgrade — no action needed.
* change: The C2B (Paybill fallback) confirmation/validation callback URLs also moved to the `ozupay_mpesa_*` prefix. OzuPay → Settings now shows the new URL to enter in your Daraja app; the old URL keeps working, so update it whenever convenient.

= 4.0.7 — 2026-08-13 =
* change: The payment-waiting screen now distinguishes "sending the STK Push" from "waiting for the customer's response to it." Previously, the moment the customer landed on the modal (or clicked Retry), the status pill immediately showed "Waiting for payment confirmation…" / "Check your phone for the M-Pesa prompt" even while the STK-send request to our own server was still in flight — before Daraja had even been asked to deliver a prompt. It now shows "Initiating payment…" for that window, and only switches to "Waiting to confirm payment, please wait…" once the push has actually been sent and polling for the result begins. Applies to both the auto-fired initial send and the Retry button. Ported from the Pro edition.

= 4.0.6 — 2026-08-13 =
* fix: The payment-waiting screen on the order-received page could show "Payment could not be confirmed" for a payment that actually succeeded. A customer connectivity blip during status polling (3 consecutive failed requests to admin-ajax.php) was being treated the same as a genuine Safaricom decline — permanently stopping polling and displaying a false failure, even though M-Pesa confirmation runs entirely server-side and has nothing to do with the customer's own connection at that moment. Confirmed live on ozupay.com order #1570 (Pro edition, same shared code). A network/AJAX error no longer ends polling; only a genuine timeout or a real Safaricom result does now. Ported from the Pro edition.
* fix: The Retry button could send a brand-new STK Push for an order that was already fully paid, if the customer clicked it while looking at a stale "failed" screen (see above). `ajax_retry_stk()` now re-reads the order's current, uncached status immediately before sending anything, and refuses with "already confirmed" instead of firing a duplicate prompt. Ported from the Pro edition.

= 4.0.5 — 2026-08-13 =
* fix: 4.0.4's blanket "add mpesa- prefix" fix to checkout.css over-corrected: the `.ozupay-btn`/`.ozupay-btn--*` family is genuinely shared with `templates/fallback/payment-issue.php` and was never meant to be renamed — adding `mpesa-` broke the match a second time, in the opposite direction. Rebuilt the fix from actual producer markup instead of a blind regex.
* fix: the same missed-rename pattern independently affected `assets/js/checkout.js` (the phone-number "use a different number" editor and the currency-notice de-duplication check), `assets/js/fallback.js` (the entire manual payment-verification fallback page), and `assets/js/admin-settings.js` (the settings form). Ported from the Pro edition.

= 4.0.4 — 2026-08-13 =
* fix: `assets/css/checkout.css` still targeted the pre-4.0.0 unprefixed class and ID names on the M-Pesa payment-waiting screen; the 4.0.0 identifier-prefix migration renamed the template markup to `ozupay-mpesa-*` but this file was missed, same as the JS fixed in 4.0.3. The modal never actually rendered as an overlay — no fixed positioning, no dimmed background, no styling on any of it — just unstyled content inline in the page. All selectors corrected. Ported from the Pro edition.

= 4.0.3 — 2026-08-13 =
* fix: The M-Pesa payment-waiting screen's JavaScript queried every element by its pre-4.0.0 unprefixed ID (e.g. `#ozupay-waiting-backdrop`), but the 4.0.0 identifier-prefix migration renamed the actual HTML to `#ozupay-mpesa-waiting-backdrop` and this file was missed. `init()`'s very first line bails out when the backdrop element isn't found, so none of the modal's JS ever ran on any order-received page since 4.0.0 shipped — no auto-sent STK push, no status polling, no retry button, no cancel. The order was created and the customer saw a "Complete payment on your phone" screen with the correct amount and number, but the actual Daraja STK push was never dispatched. All element-ID selectors in this file corrected to match the current template. Ported from the Pro edition, found live on ozupay.com.

= 4.0.2 — 2026-08-13 =
* fix: The M-Pesa "Complete payment on your phone" waiting screen could render twice, stacked on top of itself, on the order-received page — WooCommerce Blocks' own "Additional Information" component fires the same classic hook this uses for backward compatibility, alongside the normal thank-you page render, so the whole modal (header, status, both payment options) was being printed twice on stores using a block theme.

= 4.0.1 — 2026-08-13 =
* fix: The 4.0.0 prefix-rename migration missed two cron hooks that are only ever referenced through a class constant rather than a literal at the scheduling call site (telemetry, C2B shadow recheck) — found on demo.ozupay.com after upgrading, where the old-named events kept firing with no listener attached (harmless, but wasteful). A follow-up migration step now clears these too. Also fixed a stale option name in this readme's upgrade-to-Pro FAQ answer.

= 4.0.0 — 2026-08-13 =
* change: Every OzuPay-defined WordPress identifier — options, order-meta keys, cron hooks, AJAX actions, custom hooks (`do_action`/`apply_filters`), script/style handles, admin-menu slugs, and PHP constants — now uses the `ozupay_mpesa`/`OzuPay_Mpesa`/`OZUPAY_MPESA` prefix instead of `ozupay`/`OzuPay`/`OZUPAY`, per the WordPress.org reviewer's requirement for a more distinctive prefix. Existing settings and order data migrate automatically on upgrade — no action needed. Any custom code hooking into an OzuPay action or filter (e.g. `ozupay_payment_confirmed`) must be updated to the new hook name.

= 3.4.3 — 2026-08-13 =
* docs: Fixed `class-ozupay-loader.php`'s docblock, which claimed C2B hooks were "intentionally absent" from Free even though C2B confirmation/validation callbacks are registered and Free auto-matches Paybill payments made outside the STK prompt by account reference. Also removed an orphaned comment claiming the manual verification fallback page is Pro-only — it's a documented Free feature.
* docs: Added a readme entry for the free edition's automatic Paybill payment matching (previously undocumented), and reworded the Pro "C2B Buy Goods Fallback" bullet to be specific to Till/Buy Goods reconciliation, which is the part that's actually Pro-only — Till has no account-reference field to auto-match against, unlike Paybill.
* docs: Removed the readme FAQ's instruction to use the Sandbox Testing panel's "Inject Test Callback" feature, which no longer exists (removed in 3.4.2), and two leftover references to it and to the removed "C2B URL Registration" health-check row in the Sandbox Testing panel's own on-screen copy.

= 3.4.2 — 2026-08-13 =
* change: Removed the "requires a Pro license" upsell card shown on the WooCommerce order edit screen — a routine workflow screen should show payment data only, not a promotion.
* change: Removed four Sandbox Testing panel actions (C2B simulation, synthetic callback injection, B2C SecurityCredential generation, B2C test payout) that registered AJAX actions with no way to reach them from the UI and only ever responded "is a Pro feature" — no functionality was ever actually reachable through them.
* change: Removed the Daraja Health Check panel's "C2B URL Registration" row, which always reported "Pro feature" without performing an actual check — C2B Paybill/Till fallback is already a free feature.
* change: Removed dead code with no way to ever run in the free edition: an unused premium-feature registry, an unreachable email QR-code renderer, and an email "adopt website colors" toggle with no corresponding Free setting.
* fix: Copy-to-clipboard buttons (order panel, Transaction Log) no longer print inline style/script tags on every admin page load — the CSS/JS now load through the plugin's standard enqueue path.

= 3.4.1 — 2026-08-13 =
* security: Credentials still stored in the legacy AES-CBC format from before OzuPay's move to AES-256-GCM are now automatically re-encrypted to GCM the next time they're read, closing a residual gap where a database attacker could alter an old-format ciphertext without detection (GCM already rejects tampering; the legacy format could not). A credential that can't be migrated (corrupted, or the site's encryption keys changed) is now flagged on the Daraja Health Check page instead of failing silently.
* security: The Daraja Health Check page now warns if the Safaricom callback IP allowlist is empty or contains an unusable entry — previously a plugin/theme conflict or misconfiguration here could silently reject every genuine Daraja callback with nothing visible until a customer reported an unconfirmed payment.

= 3.4.0 — 2026-08-12 =
* security: OzuPay admin pages no longer suppress WordPress, WooCommerce, and other plugins' admin notices. Critical update, security, and migration notices are now visible on every wp-admin screen, including OzuPay's own — required for WordPress.org submission (no admin dashboard hijacking).
* change: Removed the standalone "Upgrade to Pro" admin page, the Pro-features dashboard grid, the floating help/upsell widget, the sandbox panel's "Order Confirmation — Pro" promotion, the Pro-feature cards on Settings → Features, the "Get OzuPay Pro" link on the Plugins list row, and the duplicate "Get Pro" quick link on the About page. Pro is now mentioned in exactly one place, the About page's banner — required for WordPress.org submission (excessive/repeated promotion).
* change: The optional diagnostic telemetry setting is now labelled "Share optional diagnostic telemetry (includes your site URL)" instead of "Share anonymous usage data" — the report includes the site's hostname, so it was never anonymous. The readme's "GDPR compliance" claim was replaced with a factual description of the WooCommerce privacy export/erasure integration.
* feat: Added suggested privacy-policy language under Settings → Privacy → "Add the suggested privacy policy text", covering what OzuPay sends to Safaricom, what it stores, retention periods, and how customer data can be exported or erased.
* fix: The free edition no longer creates the Pro-only `ozupay_daily_analytics` and `ozupay_customer_risk` database tables on activation — Free never reads or writes them, and Pro creates them itself on its own activation, so this was an unused table carrying customer phone numbers and a risk score with no purpose in Free.
* docs: Added the GPLv2 LICENSE file to the plugin package; reordered the readme's Requirements section so the WooCommerce dependency is listed first.
* docs: Corrected three stale "AES-256-CBC" claims (readme.txt, the health check panel, and the Settings page credential note) to AES-256-GCM, the cipher actually used since the earlier GCM migration; CBC is legacy read-compatibility only. Also fixed the readme naming the wrong WordPress constant (AUTH_SALT, which the encryption key is not derived from) instead of SECURE_AUTH_KEY.

= 3.3.3 — 2026-08-11 =
* fix: A Till/Buy Goods payment confirmed correctly via STK Push could still be logged as an unmatched C2B payment and fire the ozupay_c2b_unknown_payment webhook. Safaricom delivers the STK confirmation and an independent C2B "shadow" confirmation for the same payment with no guaranteed order between them, and the existing check (3.2.3) only caught the common case of STK arriving first. When the C2B shadow arrives first instead, it now waits briefly and re-checks before logging it as unmatched, giving the STK confirmation time to land. Matches the fix in OzuPay Pro 5.7.2.

= 3.3.2 — 2026-08-10 =
* fix: The M-Pesa QR code component could arrive as a broken image with some mail-sending plugins (e.g. Post SMTP), which rebuild the outgoing message in a way that drops inline images added the standard WordPress way. It's now embedded directly in the email so it no longer depends on how the site's mail plugin constructs the message. Matches the fix in OzuPay Pro 5.7.1.

= 3.3.1 — 2026-08-10 =
* fix: ozupay.payment.initiated only fired when an STK Push was sent from the POS/cashier app — a regular customer checkout's STK send fired an unrelated, unlisted action instead, so a webhook subscriber never saw an "initiated" event for a self-checkout payment. It now fires from the checkout retry path too. Matches the fix in OzuPay Pro 5.6.2.

= 3.3.0 — 2026-08-10 =
* feat: The M-Pesa receipt shown on the order screen's OzuPay panel and in the Transaction Log now has a copy icon next to it, so it can be copied without triple-clicking to select it. Matches OzuPay Pro 5.6.0.

= 3.2.3 — 2026-08-10 =
* fix: A Till/Buy Goods payment confirmed via STK Push could still be logged as an unknown/unmatched C2B payment (and fire the matching webhook action), because Safaricom also delivers an independent C2B "shadow" notification for the same receipt with no account reference to match. The C2B handler now checks whether the receipt is already confirmed on an order before logging it as unmatched. Matches the fix in OzuPay Pro 5.4.2.

= 3.2.2 — 2026-08-10 =
* change: Transaction Log searched the M-Pesa log with a leading-wildcard LIKE that couldn't use any existing index, degrading to a full table scan on every keystroke as the log grew. Added a composite index on (environment, created_at) matching the page's default filters, so search only scans the already-narrowed slice of rows. Matches the fix in OzuPay Pro 5.4.1.

= 3.2.1 — 2026-08-09 =
* fix: The STK callback's duplicate-payment check could read a stale, pre-lock copy of the order, so it didn't reliably see a confirmation the C2B callback had just committed for the same receipt while holding the same shared lock. Matches the fix in OzuPay Pro 5.3.1 — see that changelog entry for the full production incident this was found from.

= 3.2.0 — 2026-08-07 =
* security: OzuPay's admin menu and its pages (Dashboard, Transaction Log, Documentation, Upgrade, About) only required WooCommerce's generic "Manage WooCommerce" capability, so any role granted that capability for an unrelated reason — a custom "SEO Manager" role, for example — could see the entire OzuPay menu, including the Transaction Log's M-Pesa receipts and phone numbers, with none of OzuPay's own permissions ever granted. Every page and menu entry now requires the plugin's own `ozupay_manage_credentials` capability instead, matching the same fix already shipped in OzuPay Pro 5.2.3.
* security: The payment-waiting screen's `ozupay_check_payment_status`, `ozupay_retry_stk`, and `ozupay_cancel_order` AJAX endpoints verified the order key but never confirmed the order actually used an M-Pesa gateway. Anyone holding a valid order ID and order key for any pending order on the site — placed with any payment method — could call the retry endpoint and trigger a real STK Push, under the merchant's own Paybill/Till identity, to a phone number of their choosing. Each endpoint now also confirms the order's payment method before acting.
* security: The manual-code verification endpoint reachable at `/wc-api/ozupay_verify_manual` checked only a nonce shared across every logged-out visitor before attaching a customer-submitted M-Pesa code to an order, unlike its two sibling endpoints on the same Payment Issue page (`handle_poll`, `handle_resend_stk`), which both also confirm the requester holds that order's key. It now does the same.
* security: A manual M-Pesa code's Transaction Status confirmation (`class-status-callback.php`) could be processed twice by two near-simultaneous results, causing a duplicate `payment_complete()` call — unlike the STK and C2B callback paths, it never acquired the shared per-receipt lock those two paths use specifically to prevent that race. It now does.
* fix: Deleting OzuPay from wp-admin while OzuPay Pro remained installed (active or not) could permanently delete Pro's live Daraja credentials, callback token, and `ozupay_manage_credentials` capability grant — the uninstall handler's "remove all data" option only skipped dropping the shared database tables in that situation, not deleting the shared settings option, capabilities, or unscheduling cron jobs Pro still depends on. All of these now respect the same guard.
* fix: The one-time settings migration's legacy `wc_ozupay_settings` option, and a duplicate list entry, are now also cleaned up correctly during a "remove all data" uninstall.
* fix: The Payment Issue recovery page (shown to a customer when an STK Push fails) enqueued `assets/js/fallback.js` and `assets/css/fallback.css` — neither file existed in this edition, so the "Resend M-Pesa Prompt" button, the manual verification-code form, and payment-status polling on that page had no JavaScript behind them at all. Both files are now included, matching OzuPay Pro.
* fix: The Transaction Log's order links used `get_edit_post_link()`, which only resolves correctly for legacy, non-HPOS orders — on a store with High-Performance Order Storage enabled, a link could point at an unrelated `wp_posts` row that happens to share the same numeric ID. It now uses the same HPOS-aware link logic already used elsewhere in the plugin.
* fix: Two admin sandbox-testing screens rendered a Daraja API response's raw text directly into the page instead of treating it as plain text — not exploitable by an outside attacker (both actions require the credential-management capability and a nonce), but the response text originates from Safaricom's own API, so it's now inserted safely regardless.
* fix: The Daraja API client's GET request path (used by the STK Push status query) didn't check the HTTP status code before treating the response body as a successful result, unlike its POST path — an error response could be read back as if it were real data.
* fix: `OzuPay_DateTime::parse_local_mysql()` and `next_local_time()` could throw an uncaught fatal error on a malformed input value instead of a catchable one.
* fix: The "Retry" button's failure message could show Safaricom's raw API error text directly to the customer instead of a generic, translated message.
* fix: Fixed a documentation comment describing M-Pesa amounts as always rounding up ("ceiled") when the code has always rounded to the nearest shilling — no behaviour change, the comment was wrong, not the code.
* fix: Daily telemetry no longer decrypts the stored Daraja credentials just to check whether they're set — an existence check on the still-encrypted value answers that identically.
* fix: A blocked, non-Safaricom IP address on the C2B callback endpoint received a response in the integer-coded format used by the STK/Status endpoints instead of the string-coded format C2B's own protocol expects. Both are rejections either way, but now in the correct shape.
* fix: The order panel's underpayment status is now included in `OzuPay_Order_Meta::get_all()`'s aggregate, alongside every other payment-status field it already returns.
* fix: The "Payment Verified" customer email now shows the amount paid and M-Pesa receipt number — both were already being recorded but never displayed.
* dev: Removed two unreachable code paths left over from earlier refactors: an unused "Documentation" admin page (the Documentation menu item has always linked straight to ozupay.com/docs) and an unused single-feature upsell card.
* security: The Payment Issue page's poll and resend-STK endpoints — reachable by anyone holding a valid order ID and order key — never confirmed the order actually used the M-Pesa gateway, the same gap already closed elsewhere on the same page. The resend endpoint could trigger a real STK Push against any pending order regardless of its payment method; both endpoints now check the gateway first.
* security: The STK callback's confirmation write could, in principle, overwrite a real customer phone number with a masked M-Pesa privacy token if Safaricom's `PhoneNumber` field is ever masked the way its `MSISDN` equivalent already can be — it's now guarded the same way the C2B callback's identical write already is.
* fix: The STK callback's acknowledge-and-detach step ran outside the section protected by its own concurrency lock — if it threw (a transient output-buffering error), the lock could strand for up to 5 minutes and block a legitimate Daraja retry. It's now inside the protected section.
* fix: A rejected M-Pesa reversal could display as a successful payment in the Transaction Log due to a raw integer cast instead of the codebase's normal result-code handling.
* fix: Transaction Log's column sort now only accepts the columns actually meant to be sortable, matching the same fix in OzuPay Pro.

= 3.1.0 — 2026-08-07 =
* feat: Every saved credential now shows a fingerprint — its length, first and last few characters, and a short hash — so you can confirm which value is stored and whether it has changed, without the credential ever being displayed. Matches the same feature already available in OzuPay Pro.

= 3.0.3 — 2026-08-07 =
* fix: The one-time settings migration (`wc_ozupay_settings` → `ozupay_settings`) could wrongly conclude it had already run and permanently strand real settings under the old key. It only checked whether the new option name held *any* value, not whether that value looked like real settings — a disconnected bug in OzuPay Pro's setup wizard could leave a stray partial value under the new name on a site that had both editions installed, which was enough to trip the "already migrated" guard here too. The check now confirms the new option actually looks like real settings before skipping, and if a stray partial value is found alongside genuine legacy settings, the two are merged rather than the legacy settings being discarded.

= 3.0.2 — 2026-08-07 =
* fix: An M-Pesa Till (Buy Goods) STK Push payment could be confirmed by Safaricom but never complete its order. Safaricom delivers two callbacks for this payment type — an STK callback that identifies the order, and a separate C2B confirmation that structurally never can (it carries no account reference). Both callbacks were sharing one concurrency lock, so whichever arrived first could hold it for the full duration of a lookup that was guaranteed to fail, blocking the STK callback — the only one that could actually confirm the order — from ever getting a turn. The C2B confirmation now only contends for that shared lock once it has actually found a matching order.

= 3.0.1 — 2026-08-07 =
* fix: Removed the plugin header's "Domain Path" declaration, which pointed at a `languages` folder that doesn't exist in this edition — flagged by WordPress.org's Plugin Check tool. No bundled translations are shipped, so nothing to point at.

= 3.0.0 — 2026-08-06 =
* change: The plugin's settings option was renamed from `wc_ozupay_settings` to `ozupay_settings` (WordPress.org requires option names to use a unique prefix, not one starting with `wc_`). Existing settings migrate automatically on the first page load after updating — no action needed, and upgrading to or from OzuPay Pro still preserves your configuration.
* fix: Removed unused code that could technically send M-Pesa failure-notification emails if the underlying setting were ever set outside the plugin's own UI, even though Settings never exposed a way to turn it on. Admin Alerts remain an OzuPay Pro feature with no equivalent code path in the free edition.
* change: Renamed two internal PHP class names (the M-Pesa gateway and the COD-on-delivery placeholder) to drop a `WC_` prefix reserved for WordPress/WooCommerce core, per WordPress.org naming-convention requirements. No effect on existing orders, settings, or the payment method shown at checkout.
* change: Clarified the Pro upsell wording on the WooCommerce order screen — the free edition already uses the same STK Push/status-check mechanism for its own checkout and reconciliation; only the one-click retry, status recheck, and payment-link tools on that screen require Pro.
* change: Two core plugin functions were wrapped in `function_exists()` checks around their own definitions — flagged by WordPress.org because that pattern can silently swap in an unrelated plugin's same-named function instead of failing loudly. They're now gated on the plugin's own edition marker instead, which only OzuPay Free/Pro themselves ever set, so an unrelated plugin can no longer affect them; the underlying purpose (letting Free and Pro coexist without a fatal error if both are ever active) still works exactly as before.

= 2.1.2 — 2026-08-05 =
* fix: A confirmed M-Pesa payment could fail to complete its order on sites running a persistent object cache (e.g. Redis) alongside WooCommerce's high-performance order storage. The STK callback's order lookup ran in its own process, separate from whichever request had just saved the order's CheckoutRequestID, and could occasionally read a stale, empty result a few seconds later — a genuine, already-successful payment then sat unlinked in the transaction pool instead of completing the order. The lookup now falls back to a direct database query that bypasses the cache entirely whenever the cached lookup finds nothing, before ever concluding a payment can't be matched.

= 2.1.1 — 2026-08-05 =
* fix: The payment screen's Retry button could be clicked within seconds of the M-Pesa prompt being sent, firing a second STK push while the first was still in flight — racing it for the same phone number and wasting a retry attempt on a request the customer never distinctly saw. The button's initial state was governed only by how many retries were left, with nothing accounting for whether a push was actually still pending. It now starts disabled by default and only renders active on load when the order already carries a known failure from a previous attempt; it still re-enables the moment that failure or a timeout is confirmed.

= 2.1.0 — 2026-08-05 =
* feature: An order whose M-Pesa STK push callback never arrives is now rechecked with Safaricom within minutes instead of waiting up to half an hour. Previously, if a customer's browser tab closed before the confirmation callback landed, nothing looked at that order again until the next 30-minute reconciliation sweep. A one-off check is now scheduled for every STK push at the same 3-minute grace period the sweep itself already waits on, reusing the sweep's own logic exactly — it skips the check entirely if the payment screen's own status check already covered the same moment, and a retry always moves the check to the retry's own time rather than leaving a stale one behind. The 30-minute sweep is unchanged and remains the guaranteed fallback either way.

= 2.0.16 — 2026-08-03 =
* fix: The payment screen no longer gets stuck on "Still checking with M-Pesa…" after it has given up. When the 90-second check expired, the status request already in flight was left running, and its reply a moment later overwrote the "we could not confirm this payment" message with the waiting one — so the screen sat there indefinitely. The in-flight request is now cancelled with the timer, and a late reply can no longer undo a final message.

= 2.0.15 — 2026-08-03 =
* fix: The payment screen no longer says "Payment received" for a payment that may never have happened. Safaricom's status check only reports whether a request errored — it carries no M-Pesa receipt and no amount — but a clean result was being shown to the customer as confirmed payment, with the Retry button disabled. On a real order this left a customer whose prompt Safaricom never delivered to their phone looking at "Payment received — confirming…" with no way to pay. The screen now says "Still checking with M-Pesa…" while it waits, and only a genuine confirmation carrying a receipt is ever called a payment.
* change: When that check times out with no confirmation, the screen now says plainly that the payment could not be confirmed and asks the customer to look for an M-Pesa SMS: if one arrived, keep it and contact the store rather than paying again; if not, retry or pay manually. Retry stays reachable in that state instead of being locked, so a customer who was never charged is no longer stranded. Orders are still never marked paid without a real confirmation.

= 2.0.14 — 2026-08-03 =
* fix: A customer whose M-Pesa payment succeeded is no longer told it failed. When Safaricom accepts a payment but never delivers the confirmation callback, the payment screen now checks with Safaricom directly and shows "Payment received — confirming…" instead of counting down to "No response received". The Retry button stays disabled in that state, so nobody is invited to pay a second time for money that has already left their account.
* fix: The proactive Safaricom status check now runs in sandbox as well as production. It was previously production-only, which left sandbox — where confirmations are least reliable — with no fallback at all, so a successful sandbox payment reported a failure.
* change: Cancelling from the payment screen now checks with Safaricom in sandbox too, so "Cancel and choose a different payment method" can no longer discard an order whose payment has already gone through.

= 2.0.13 — 2026-08-01 =
* fix: Orders now confirm as soon as M-Pesa reports the payment, instead of after everything the confirmation sets off. The plugin was making Safaricom wait while confirmation emails were sent — measured at 3.4 seconds of a 3.7 second callback on a live store, with the order itself recorded in under a tenth of a second. Safaricom is now acknowledged immediately and the rest happens once the connection is closed, so nothing a site has attached to a confirmed payment — email, webhooks, or anything else — can hold up the payment being recorded. No emails or notifications are skipped or delayed; they simply no longer block.

= 2.0.12 — 2026-08-01 =
* fix: When Safaricom cannot deliver the payment prompt to the customer's phone, the checkout now says so instead of waiting in silence. That failure — reported as "no response from customer, phone may be off or out of coverage" — was being treated as a result that might still turn into a payment, so the modal stayed on "Waiting for payment" until it timed out several minutes later, telling the customer nothing and leaving them unsure whether to retry. The reason is now shown as soon as it is known, alongside the retry and pay-manually options, and a late confirmation is still picked up if one arrives.

= 2.0.11 — 2026-08-01 =
* fix: Reverts the email change made in 2.0.10, which did not work. Measuring it on a live payment showed the confirmation was no faster, and the queued emails then added a further three-second delay of their own at the end of the request — leaving confirmation slower than before the change, not faster. The previous behaviour is restored while the real cause is traced.

= 2.0.10 — 2026-08-01 =
* fix: Orders now show as paid several seconds sooner after the customer enters their M-Pesa PIN. Confirmation emails were being sent while Safaricom's confirmation was still being processed, holding it up by around 3.3 seconds. They are now queued and sent moments later instead, with no change to the emails themselves or to when the order is marked paid.

= 2.0.9 — 2026-08-01 =
* fix: The background token refresh now runs. It was checking a gateway on/off setting in the wrong place, found nothing there, and treated that as "switched off" — so on every site it exited immediately without refreshing anything. It now goes purely on whether Safaricom API credentials are saved.

= 2.0.8 — 2026-08-01 =
* fix: The 30-minute background tasks — payment reconciliation, deposit auto-cancel, and the new token refresh — now actually get scheduled. They were being registered before the plugin defined its own 30-minute interval, and WordPress discards a scheduled task whose interval it does not yet recognise without reporting an error, so on sites upgraded rather than freshly activated these three tasks were never running at all. The registration now happens later in the load order, and refuses silently failing.
* fix: The background token refresh no longer discards a working Safaricom access token when the refresh itself fails. It previously cleared the stored token before fetching its replacement, so a single failed refresh left the site with none and passed the delay on to the next customer.

= 2.0.7 — 2026-08-01 =
* fix: A successful M-Pesa payment could briefly show as failed before correcting itself to confirmed a second later. When a customer took more than 15 seconds to enter their PIN, OzuPay asked Safaricom for the payment's status and treated the "customer has not responded yet" answer as a decline. That answer is now recognised as non-final, and only a genuine cancellation is acted on straight away. A customer who saw the false failure could have paid a second time.
* fix: The M-Pesa prompt now reaches the customer's phone faster after checkout. The plugin re-authenticated with Safaricom during the payment request itself whenever its saved access token had expired, so the first checkout after a quiet period waited for two round trips to Safaricom instead of one. The token is now refreshed in the background every 30 minutes, before any customer needs it.
* fix: A scheduled task added by a plugin update is now registered on sites that were already running OzuPay. Previously only a fresh activation set up scheduled tasks, so a task introduced in an update never ran until the plugin was manually deactivated and reactivated.
* fix: Uninstalling the plugin now clears every scheduled task it created. Two were left behind and would keep firing until the site was next reinstalled or the events manually removed.

= 2.0.6 — 2026-08-01 =
* fix: Deleting the plugin assigned four unprefixed variables at global scope while it ran, which could collide with WordPress or another plugin's variables of the same name during uninstall. They are now prefixed.

= 2.0.5 — 2026-07-31 =
* security: A payment or refund result from Safaricom carrying a non-numeric code (for example "R000001", returned when a transaction has already been reversed) was treated as a success, because PHP converts such a code to zero. A rejected transaction could therefore have been recorded as completed. Any result code that is not a plain number is now treated as a failure.

= 2.0.4 — 2026-07-31 =
* fix: When Safaricom rejects a Daraja request, the reason it gives is now shown instead of a bare "Daraja HTTP 401". Previously the error code and message Safaricom returns were discarded, so every failure looked the same.

= 2.0.3 — 2026-07-31 =
* change: PHPUnit configuration files are no longer included in the distributed plugin package. They are development-only files with no effect on a live site.

= 2.0.2 — 2026-07-31 =
* fix: The Transaction Log, Settings, Upgrade and About screens rendered completely unstyled. The stylesheet was only being loaded on screens matching a hardcoded list of page identifiers that never actually matched WordPress's real ones.

= 2.0.1 — 2026-07-31 =
* fix: The OzuPay Dashboard and Upgrade screens showed "There has been a critical error on this website" instead of the Pro feature list. Removing the promotional bar in 2.0.0 also deleted the feature data those screens read from.

= 2.0.0 — 2026-07-31 =
* change: The plugin is now called "OzuPay Payment Gateway for M-Pesa" and its translation text domain changed to `ozupay-payment-gateway`, to make clear it is an independent integration and not affiliated with Safaricom or M-Pesa.
* change: The Callback Token can now be edited directly in Settings. Restoring a previous token — so callback URLs already registered with Daraja keep working after a data wipe or host migration — no longer requires OzuPay Pro.
* change: Removed the promotional banner from the OzuPay admin pages. It fetched content from ozupay.com on a background schedule and reported clicks back, without asking first. Its scheduled job and stored state are cleared automatically on update.
* change: The inactive "M-Pesa on Delivery" entry in WooCommerce → Payments no longer advertises the Pro edition. It remains listed, and only so that older orders recorded against it still show a readable payment method name.
* change: Removed the "OzuPay Pro" promotional bar that sat above the content on the Dashboard and Settings screens. Pro features are still listed on the Upgrade page, the Dashboard feature list, and the Premium Features section of Settings.
* change: Admin and checkout styles and scripts are now loaded as proper stylesheet and script files instead of being printed into the page, so browsers can cache them.
* security: Data arriving at the M-Pesa callback endpoints is now sanitised the moment it is decoded, before anything is written to the transaction log or shown in wp-admin.
* fix: The show/hide eye icon no longer appears on a credential field that is already saved and empty. Saved credentials are never sent to the browser, so there was nothing for it to reveal — clicking it looked broken. It now appears only while you are entering a new value.
* fix: The OzuPay admin stylesheet was requested on every OzuPay screen but had never been shipped, so every one of those page loads produced a 404.
* fix: Transaction Log filter labels ("All Events", "Sandbox Only", and so on) could never be translated. They now are.
* docs: The readme documents every external service the plugin contacts — Safaricom's Daraja API, and the optional, off-by-default diagnostics report — including what is sent, when, and links to each service's terms and privacy policy.

= 1.26.11 — 2026-07-31 =
* change: Clearer checkout and payment window — the amount being charged is now shown up front, the Place order button states what the customer is paying, and phone numbers appear in a readable local format.
* fix: Checkout no longer asks the customer for their phone number twice when the page already collects a billing phone.
* fix: The retry option is no longer hidden after a payment prompt fails or times out.

= 1.26.10 — 2026-07-31 =
* fix: When OzuPay rejected an unverified Paybill/Buy Goods (C2B) callback, it replied to Safaricom with the code that means "payment accepted" instead of the one that means "rejected". No payment or order was ever affected — the request was already discarded before any processing — but the reply itself was wrong, and now correctly reports the rejection.

= 1.26.9 — 2026-07-31 =
* security: On sites behind a reverse proxy (the standard Nginx setup), a visitor could put any address they liked in a forwarded-for header and have OzuPay treat it as their real IP address. That let them pose as a Safaricom server to the callback endpoints, and sidestep the per-IP cap on manual M-Pesa code verification attempts by presenting a fresh address on every try. OzuPay now reads the address its own proxy actually observed, which a visitor cannot forge. Cloudflare sites were not affected.
* fix: The "Resend M-Pesa Prompt" button on the Payment Issue page ignored the STK Push attempt limit — a customer who had used up their retries in the payment window could keep sending themselves new prompts from that page. It now shares the same attempt counter and limit as the Retry button, and a failed send no longer costs the customer an attempt.
* fix: Clearing an order's underpayment flag did not save the change on its own, relying on a later save elsewhere in the same request. Harmless today, but it would have silently lost the update from any future code path that cleared the flag without saving afterwards.

= 1.26.8 — 2026-07-31 =
* change: OzuPay's own admin pages no longer show unrelated WordPress/plugin admin notices pushing the actual content down — those still show normally everywhere else in wp-admin.

= 1.26.7 — 2026-07-30 =
* change: Transaction Log's Date column now shows the site's UTC offset alongside each timestamp — e.g. "July 30, 2026 3:45 pm UTC+03:00" — instead of the raw unformatted database value.

= 1.26.6 — 2026-07-30 =
* fix: Retrying payment from an underpaid order (Retry button, or the automatic STK on page load) still sent an M-Pesa prompt for the full original amount, not the remaining balance — the payment-waiting page displayed the correct shortfall, but the actual STK-send endpoint never checked for a flagged underpayment. Now sends only the shortfall, matching what's shown on screen.

= 1.26.5 — 2026-07-30 =
* fix: A wrong or fake M-Pesa code submitted on the Payment Issue page could never be automatically resolved — Safaricom's "no such transaction" result couldn't be matched back to the order (only a successful lookup carries a receipt number to match on), so the order sat on-hold indefinitely with the customer's page stuck on "verifying payment…" forever, no email, no admin note, nothing. A failed lookup is now correlated back to the order via the originally-queried code instead, and a definitive "not found" from Safaricom now returns the order to pending immediately so the customer can submit a corrected code.
* fix: Retrying payment from an underpaid STK/C2B order re-charged the full original amount instead of the remaining balance — the payment-waiting page and "Resend M-Pesa Prompt" now charge only the shortfall once an underpayment is flagged.

= 1.26.4 — 2026-07-29 =
* fix: An underpaid STK Push or C2B payment (real money received, just less than the order total) gave the customer no live signal at all — the payment-waiting modal just showed "Waiting for payment confirmation…" indefinitely, then eventually a misleading "timed out, retry?" that would fire a brand-new STK Push for the full order amount with zero acknowledgment that part of it was already paid. The modal now shows exactly what was received and what's still owed as soon as the underpayment is detected.
* fix: Underpayment could also leave a stale "STK declined" flag set from an earlier failed attempt, so a genuinely-received-but-short payment could be misreported as a flat decline.

= 1.26.3 — 2026-07-29 =
* fix: Clicking "Choose another payment method" while an error occurred showed no visible feedback at all — the element it wrote into didn't even exist in this edition's page (no manual-code form here). Now shown next to the Cancel button itself.
* fix: The initial "Pay via M-Pesa" button on the pay-prompt screen sent a blank phone number straight to the server instead of catching it instantly like the Retry button does.
* change: Cancelling while an M-Pesa prompt is still live on the customer's phone now shows an inline "cancel anyway?" confirmation matching the modal's own styling, instead of a native browser confirm() popup.

= 1.26.2 — 2026-07-29 =
* change: For Paybill orders with C2B Fallback active, the "already paid? contact us" note now explains that payment confirms automatically within a minute or two instead of prompting the customer to contact support right away. Till is unchanged, since it has no account-reference field for Safaricom to auto-match against.

= 1.26.1 — 2026-07-29 =
* fix: The new manual-code confirmation email now uses the same neutral icon tone and plain-text code display as every other OzuPay email template, instead of a one-off style.

= 1.26.0 — 2026-07-29 =
* feat: Customers now get a confirmation email immediately after submitting an M-Pesa transaction code for manual review, with the code and order details — previously they only got WooCommerce's generic "order on hold" email, with no mention of the code or that it was under review.
* fix: On the order-received page, the "Got it, thank you!" bar shown after submitting a payment code had a "View" button that did nothing on any later page load — the code path that shows this bar never wired up its click handler. Reopening it now also shows the right "under review" message instead of a stale payment form.

= 1.25.9 — 2026-07-29 =
* fix: Text domain corrected from "ozupay-free" to "ozupay-m-pesa-payments" throughout the plugin (plugin header and every translation function call) to match the WordPress.org plugin slug, resolving Plugin Check's TextDomainMismatch errors.

= 1.25.8 — 2026-07-28 =
* security: The Settings page no longer echoes the (previously never-decrypted, but still raw ciphertext) Consumer Key/Secret/STK Passkey back into the page HTML — closes the same unsafe pattern fixed in OzuPay Pro before it could ever become exploitable here. These fields now always render blank with a "Saved — leave blank to keep, or enter a new value to replace" placeholder when something's already stored. The Sandbox Testing panel's and Settings page's own "required field" checks still correctly recognize already-saved credentials via a new saved/unsaved flag, so existing configured stores are unaffected.

= 1.25.7 — 2026-07-28 =
* fix: Plugin header's Author URI no longer duplicates the Plugin URI (both pointed to ozupay.com) — Author URI now points to ozulabs.com.

= 1.25.6 — 2026-07-28 =
* fix: Transaction Log no longer shows a bare "No items found" when the default "Current Environment" filter doesn't match where your data actually landed (e.g. Settings still says Sandbox but the real activity is tagged Production). It now explains why and links straight to "Show All Environments".

= 1.25.5 — 2026-07-28 =
* change: OzuPay's transactional emails (payment confirmed, manual verification, etc.) now use the same brand green as the rest of the plugin for their default accent color, instead of a slightly different shade.

= 1.25.4 — 2026-07-28 =
* change: Standardized every primary action button and status color across the admin UI to one brand green instead of several different shades (About, Settings, Sandbox Testing, and the plugin dashboard's stat cards previously each used their own slightly different green).

= 1.25.3 — 2026-07-28 =
* security: STK Push's cross-order receipt reuse check is now covered by the same atomic receipt-level lock as C2B, closing a race window where a genuine STK callback and a forged/replayed callback for a different order — carrying the same receipt — could both pass the duplicate check before either write committed, since they use different CheckoutRequestIDs and therefore different outer locks.

= 1.25.2 — 2026-07-28 =
* fix: The C2B (Buy Goods/Paybill) confirmation handler no longer writes a blank, unused log row to the transaction table on every callback — leftover from a payment-pool feature that Free never had.
* security: C2B confirmation now uses the same atomic per-receipt lock as STK Push, closing a narrow race window where two near-simultaneous Safaricom deliveries for the same receipt could both pass the duplicate-payment check before either write committed.

= 1.25.1 — 2026-07-28 =
* change: The "Test STK Push" card on the C2B live testing screen now uses the same green accent as other payment-testing tools instead of red, since red is reserved for destructive/data-deletion actions.

= 1.25.0 — 2026-07-28 =
* fix: The "Test C2B With a Real Payment" card now shows your configured customer-facing Till/Paybill number instead of the internal M-Pesa business shortcode, and labels it "Till Number" or "Paybill Number" (with the matching M-Pesa menu path) based on what you actually have configured, instead of always saying "Till/Paybill".
* feat: The "Test C2B With a Real Payment" screen now includes a "Test STK Push" card so you can send a real test STK Push without leaving this screen — it uses the same guarded live STK tester (confirmation checkbox, 60-second cooldown, KES 10 cap) as Settings → Credentials.

= 1.24.2 — 2026-07-28 =
* fix: The Sandbox Testing panel's "Send STK Push" tool is now hidden while Environment is set to Production instead of staying visible but non-functional. Use "Test STK Push" on Settings → Credentials to send a real STK Push in Production.
* feat: Production mode now shows a "Test C2B With a Real Payment" card with step-by-step instructions for verifying Buy Goods/Paybill payments with a real KES 1 transaction and a link to the Transaction Log — Safaricom's C2B Simulate has no Production equivalent, so this replaces guesswork with a documented path.

= 1.24.1 — 2026-07-28 =
* fix: Transaction Log now shows "Masked by Safaricom" instead of a raw, unreadable privacy token in the Phone column when Safaricom didn't send a real phone number.

= 1.24.0 — 2026-07-27 =
* feat: Every transaction is now tagged with the Sandbox/Production environment that was active when it was recorded, so switching Settings from Sandbox to Production no longer mixes old test transactions into your production data.
* feat: The Transaction Log now defaults to showing only the currently active environment's data, with a new filter to switch to Sandbox, Production, or All Environments on demand.

= 1.23.6 — 2026-07-27 =
* fix: C2B payments where Safaricom sent a masked/privacy-tokenised phone number (since Safaricom's March 2026 M-Pesa number masking rollout) could fail to record correctly in the payment log.
* security: A C2B payment's masked Safaricom phone token can no longer overwrite a real customer phone number already on an order.

= 1.23.5 — 2026-07-27 =
* security: Hardened the order-verification checks on the payment-waiting and STK-resend endpoints against timing-based attacks.
* security: Added a rate limit to the "Resend M-Pesa Prompt" button so it can no longer be used to repeatedly spam a customer's phone with STK prompts.
* security: Closed a race condition that could let two simultaneous STK retry requests exceed the configured retry limit.

= 1.23.4 — 2026-07-27 =
* fix: On checkout themes with an intentionally dark page background, the M-Pesa on Delivery deposit summary box ("Pay Now (Delivery Fee)" / "Pay on Delivery (Products)") and the "no deposit required" notice used a translucent green tint that composited to near-black against the dark page, making the dark summary text unreadable. Both now use a solid background so they stay legible regardless of the surrounding page's background color.

= 1.23.3 — 2026-07-27 =
* fix: On checkout themes with dark-mode text/background resets, the M-Pesa on Delivery deposit summary box ("Pay Now (Delivery Fee)" / "Pay on Delivery (Products)") could render dark text on a dark background, making both lines unreadable.

= 1.23.2 — 2026-07-27 =
* fix: The STK Push reconciliation sweep's 24-hour give-up window, the stale-callback-lock window, and the API-health-check activity windows could run a few hours later or earlier than intended on stores whose WordPress timezone isn't UTC.
* fix: Transaction Log showed a blank Result for successful M-Pesa payments (STK, C2B, and manual code confirmations) — the confirming event never recorded its success code, so the Result column showed "—" even though the order was paid.

= 1.23.1 — 2026-07-24 =
* fix: The Environment dropdown's arrow overlapped the "Production (Live)" text in the new credentials cards — dropdowns now use a custom arrow with proper spacing so it never collides with the selected option's label.
* fix: Consumer Secret and other long Daraja secrets got clipped by the field's fixed width when revealed via the eye icon. Revealed fields now grow to fit their full value and shrink back once hidden again.

= 1.23.0 — 2026-07-24 =
* feat: Test Credentials, Test STK Push, and every button in the Sandbox Testing panel (Test OAuth Token, STK Push, Generate Security Credential, B2C Payout) now check that their required fields are actually filled in first — Business Shortcode, Consumer Key/Secret, STK Passkey, or the button's own local fields — and show an inline warning instead of sending an incomplete request to Daraja.

= 1.22.10 — 2026-07-24 =
* fix: Text inside the Settings → Credentials cards sat almost flush against the card's left edge — the horizontal padding was set on the outer table element, which some browsers ignore for tables. Padding now lives on each row instead, so labels, inputs, and descriptions are properly inset.

= 1.22.9 — 2026-07-24 =
* change: Settings → Credentials cards now use a softer look (white cards, subtle shadow, rounded inputs with a focus glow) instead of the plain grey table styling.
* change: Business Information is now the first section under Settings → Credentials and the only one open by default; Daraja API Credentials, Callback Configuration, and Logging & Display start collapsed.

= 1.22.7 — 2026-07-24 =
* fix: Credentials sections (added in 1.22.6) had fields laid out in the old two-column label/input table style, leaving a large empty gap between the label and the field inside the new card. Fields now stack label-above-input within each collapsible section.

= 1.22.6 — 2026-07-24 =
* change: Settings → Credentials is now broken into collapsible sections (Daraja API Credentials, Business Information, Callback Configuration, Logging & Display) instead of one long scrolling form. Each section remembers whether you left it open or collapsed.

= 1.22.5 — 2026-07-24 =
* change: The sandbox default value boxes for STK Passkey and Business Shortcode now explain what the value is (Safaricom's shared test credential for the Daraja sandbox app) and that it only works with sandbox, not just showing a bare "Use this" button.

= 1.22.4 — 2026-07-24 =
* change: In Settings → Daraja Credentials, STK Passkey now appears directly below Consumer Secret instead of further down the form.

= 1.22.3 — 2026-07-21 =
* security: Order confirmation emails now escape a couple of values (a border style and a step number) before printing them into the HTML template, closing a theoretical output-injection gap in the deposit/order-summary rendering.

= 1.22.2 — 2026-07-20 =
* fix: Telemetry no longer reports "callback unreachable" for sites where the callback endpoint is actually fine. The health check's self-probe (the site requesting its own callback URL) can fail on hosts that block server-to-self requests, even though real Daraja callbacks reach the site without issue — this false result is no longer sent as telemetry.

= 1.22.1 — 2026-07-20 =
* change: Redesigned the promo/update banner to be calmer and less intrusive — soft tinted background with a thin accent bar instead of a heavy border and shadow, a rounded pill-shaped button, a low-opacity dismiss control that only darkens on hover, and a subtle fade-in instead of popping in abruptly. No icons.

= 1.22.0 — 2026-07-20 =
* change: The promo banner fetch no longer runs on an admin page load — it's now refreshed only by a background twicedaily cron job, so a cold or expired cache never adds network latency to opening an OzuPay admin screen. A fresh install (or a site that missed its scheduled tick) kicks a one-off background fetch instead of waiting up to 12 hours for the first check.
* change: The promo banner no longer uses WordPress's shared `admin_notices` hook — it's now rendered directly by each OzuPay admin page (Dashboard, Transaction Log, Settings, Upgrade, About) at a fixed spot near the top, so it can never be pushed around or visually crowded by unrelated plugins' own notices.
* feat: The promo banner now supports a Severity color (promo/info/warning, set on the license server) and can be marked non-dismissible for must-see notices. Clicking the banner's link now reports back to the license server for basic click-through tracking.
* feat: The same banner spot now also shows a "new version available" notice when there's a pending OzuPay update and no configured promo banner is active — a configured banner always takes priority, and the spot shows nothing at all when neither applies.

= 1.21.0 — 2026-07-19 =
* feat: Every OzuPay admin screen (Dashboard, Transaction Log, Settings, Upgrade, About) can now show an occasional promo banner (announcements, offers) configured centrally on the license server. It's dismissible per site — dismissing hides it for 5 days (sooner if the banner content changes, since that counts as a new banner) — and checks for a new banner at most every 12 hours.

= 1.20.2 — 2026-07-19 =
* change: The Test STK Push result message on Credentials now reflects the saved Environment. In Production it still warns that a completed charge must be reconciled manually via your Safaricom business dashboard; in Sandbox it now explains the transaction is simulated and the amount auto-reverses, with no manual reconciliation needed. The confirmation checkbox and warning box only appear when Production is the saved environment.
* fix: Consumer Key, Consumer Secret, and STK Passkey fields on the Credentials tab were missing the "eye" icon used to reveal a saved value, and never showed the saved value at all (only a placeholder). They now match Pro: the saved value can be revealed and hidden with the eye icon.

= 1.20.1 — 2026-07-19 =
* change: Transaction Log's sort-column/direction request parameters are now sanitized with sanitize_key() before being passed to the SQL ORDER BY builder, matching Pro's defense-in-depth for this input.
* change: Sandbox testing's AJAX handler now verifies the nonce before reading the requested test action, matching Pro's request-handling order.

= 1.20.0 — 2026-07-19 =
* fix: A legacy COD-deposit order (from a prior Pro install) confirmed via manual M-Pesa code entry was moved to "Processing" (looking fully paid) instead of "Awaiting COD Balance". The manual-verification path now matches the STK Push path and correctly leaves it awaiting the balance.
* fix: A legacy COD-deposit order paid manually via Paybill/Buy Goods (C2B) was matched against the full order total instead of the deposit amount, wrongly flagging a correct deposit payment as underpaid — and the BillRefNumber lookup didn't strip the "-DEP" suffix at all, so the order often failed to match. The C2B path now handles deposit orders the same way Pro does.
* fix: The "STK Push retry attempts" setting had no effect — checkout still hard-capped retries at a fixed 3 attempts regardless of what was configured. It now reads the configured limit.

= 1.19.5 — 2026-07-19 =
* change: The "Save Settings" control on OzuPay → Settings is now a floating pill button (matching Pro) instead of a full-width bottom bar, positioned above the existing Help/Documentation/Feature Request/Contact Us button.
* fix: The floating Documentation/Request a Feature/Contact Us button never rendered on any OzuPay admin page because its visibility check compared the screen ID against the text domain ("ozupay-free") instead of the actual screen-ID slug ("ozupay"), so the condition was always false.

= 1.19.4 — 2026-07-19 =
* fix: The floating "Save Settings" button on OzuPay → Settings only appeared on the "Plugin Data" tab due to a misplaced HTML tag, making it look like the button was missing entirely on the Credentials and Features tabs. It now shows on every tab, matching the Pro edition.

= 1.19.3 — 2026-07-19 =
* change: Callback Token field is now read-only in the free edition. Manually editing or restoring it (to keep the same callback URLs after a data wipe) requires OzuPay Pro; the free edition shows a notice pointing to it instead.

= 1.19.2 — 2026-07-19 =
* fix: SSL health signal now correctly reports true in WP-CLI and cron contexts by falling back to the site URL scheme when is_ssl() and HTTP headers are unavailable.

= 1.19.1 — 2026-07-19 =
* fix: Callback URL fields (STK/C2B) were cut off in a narrow, fixed-width box, hiding most of the URL. They now expand to show the full URL.
* change: Renamed the "Advanced" settings tab to "Plugin Data".
* change: Payment Display Mode now explains what Modal and Dedicated Page actually do, and clarifies that Dedicated Page silently falls back to Modal whenever the checkout page uses the WooCommerce Checkout block.

= 1.19.0 — 2026-07-19 =
* feat: Added a "Callback Token" field (OzuPay → Settings → Credentials) showing the secret behind the token= parameter in your callback URLs, with Copy and Regenerate buttons. Previously this token was only auto-generated and silently reset whenever the plugin's data was wiped (uninstall, restoring an old backup, migrating hosts), forcing a full re-registration of every callback URL with Daraja. Back up this value once, and pasting it back after a wipe restores your exact previous URLs instead.

= 1.18.1 — 2026-07-19 =
* change: OzuPay → Settings is now organized into three tabs (Credentials, Features, Advanced) instead of one long scrolling page.

= 1.18.0 — 2026-07-18 =
* feat: When the store currency isn't set to Kenyan Shilling (KES), the M-Pesa payment method now stays visible at checkout but appears greyed out and unselectable, with a notice explaining that KES is required. Previously it disappeared from the checkout entirely, which looked like a bug rather than a configuration issue.

= 1.16.0 — 2026-07-18 =
* feat: Added a "Till/Store Number (PartyB)" setting for merchants whose Till was migrated by Safaricom under a Head Office (HO) structure. Leave blank for a standard (non-HO) Till or Paybill; behavior is unchanged.

= 1.15.2 — 2026-07-18 =
* security: The Transaction Log's "Show Raw Payload" (and the underlying database rows) exposed the STK Push 'Password' and Transaction-Status 'SecurityCredential' fields in plain text, from which the STK Passkey was trivially recoverable. These fields are now redacted before logging, and a one-time cleanup scrubs them from existing historical rows on upgrade.
* fix: STK Push failures with Daraja result code 2002 now show a specific, actionable message instead of a generic fallback.

= 1.15.1 — 2026-07-17 =
* change: Test Credentials and Test STK Push in Settings now print the full Daraja API response, not just a one-line summary.

= 1.15.0 — 2026-07-17 =
* feat: Added a "Test STK Push" button in Settings → Daraja Credentials that sends a real STK Push prompt in whichever environment is currently selected, including Production. Requires an explicit confirmation before it's clickable, caps the test amount at KES 10, and enforces a 60-second cooldown between sends.

= 1.14.0 — 2026-07-17 =
* feat: Added a "Test Credentials" button in Settings → Daraja Credentials that requests a live access token from Daraja using the saved Consumer Key/Secret, for whichever environment (sandbox or production) is currently selected.

= 1.13.5 — 2026-07-17 =
* security: The per-installation callback security token (a second layer alongside the Safaricom IP whitelist) silently stopped being enforced if the option holding it was ever missing — which could happen on any site that was updated by file deploy rather than a genuine plugin reactivation. Callbacks are now rejected outright if the token is missing instead of the check being skipped, and the token is automatically regenerated the moment it's found missing so this can't recur; an admin notice then prompts re-registering C2B URLs with Safaricom since the new token invalidates any URLs already on file with them.
* change: The Callback URLs field in Settings now explains, in a highlighted warning box, that the token in each URL is per-installation and that wiping the plugin's data regenerates it, requiring the URLs to be re-registered with Daraja.

= 1.13.4 — 2026-07-10 =
* change: The M-Pesa gateway class name still trips a Plugin Check naming-convention warning for the required `WC_` WooCommerce prefix; suppressed with a documented reason. No behavior change.

= 1.13.3 — 2026-07-10 =
* fix: The M-Pesa payment-waiting screen had 39 strings using the Pro plugin's text domain instead of the free plugin's, which broke translation loading for that whole screen (flagged by WordPress.org's Plugin Check).
* change: The duplicate manual-payment-code check no longer uses a database query pattern (post exclusion) flagged as a performance risk by WordPress VIP standards; it's replaced with an equivalent, index-friendly lookup.
* change: readme.txt "Tested up to" updated to WordPress 7.0.
* change: Internal PHPCS/Plugin Check suppression comments for the callback lock's database queries were on the wrong lines and not actually suppressing what they claimed to; corrected (no behavior change).

= 1.13.2 — 2026-07-10 =
* fix: Clicking "Choose another payment method" while an M-Pesa STK prompt was still live on the customer's phone silently cancelled the order without warning — if the customer then entered their PIN anyway, the order was quietly resurrected behind their back. The plugin now checks with Safaricom first: if the prompt is still pending, the customer is warned before cancelling; if it already succeeded, cancellation is blocked instead of racing the payment.
* change: A settings-page description that said "Paybill" now correctly says "Paybill/Till", since C2B fallback supports either shortcode type.

= 1.13.1 — 2026-07-11 =
* change: Synthetic STK callback injection in the Sandbox Testing panel is now Pro-only, consistent with the other injection-style sandbox tools (C2B simulate, B2C). The free panel keeps credential prefill, the OAuth token test, and sandbox STK Push.

= 1.13.0 — 2026-07-11 =
* feat: The Transaction Log admin page now shows the full searchable, filterable log of Daraja API events (it previously showed an upgrade notice even though events were being recorded).
* feat: The Daraja Health Check panel now renders on the Settings page — pass/fail indicators for credentials, SSL, callback receipt, environment, and recent failure rate.
* feat: The Sandbox Testing panel now renders on the Settings page when Environment is set to Sandbox — test your Daraja credentials, trigger a sandbox STK Push, and inject test callbacks without leaving wp-admin.
* feat: Manual verification fallback is now fully functional — customers who paid via Paybill/Till can submit their M-Pesa receipt code for confirmation, and the payment-issue page linked from the manual-instructions email now renders instead of returning a 404.

= 1.12.0 — 2026-07-07 =
* fix: A successful M-Pesa payment caused a PHP error inside the payment confirmation callback (a leftover reference to a Pro-only POS feature). The order was still marked paid, but the confirmation event and integrations hook never fired and Safaricom received an error response. This is now fixed.
* security: A manual Paybill payment confirmation (C2B) presenting an M-Pesa receipt that had already been used to confirm a different order could confirm a second order with the same single payment. Receipts are now checked against all orders — the same protection the STK Push callback already had.
* fix: Orders with a fractional total (e.g. KES 999.49) paid manually via Paybill were flagged as underpaid and put on hold, because M-Pesa only moves whole shillings while the plugin compared against the fractional total. The comparison now uses the same rounded amount the customer was asked to pay.
* fix: A malformed (non-JSON) payload sent to a payment callback endpoint no longer triggers an internal error in the logging layer; it is now handled as an invalid payload.
* feat: The M-Pesa payment screen now opens before the STK prompt is sent to the customer's phone, so the customer sees "Check your phone" before their device rings (previously the phone could ring while the browser was still loading).
* fix: When the customer dismissed the M-Pesa prompt on their phone, the payment screen could stay in "Waiting for payment confirmation…" for up to 90 seconds. Cancellation is now detected within about 15 seconds via the Daraja status query.
* fix: On sites using a persistent object cache (e.g. Redis), the payment screen could fail to notice a declined/cancelled STK Push because the status poll was reading stale cached order data.
* fix: A Daraja API error response (e.g. HTTP 500) is no longer treated as a normal response — it is now recognised and logged as an error.
* fix: The daily telemetry report (opt-in) now reschedules itself if its cron job goes missing, and an initial report is sent right after activation instead of up to 24 hours later.

= 1.11.0 — 2026-07-07 =
* fix: "View order details" on the payment-waiting screen reloaded the page with the same flag that re-opens the waiting modal, so customers were sent right back into it instead of seeing their order. It now links to a plain order-received URL so the customer actually sees their order status.
* fix: If cancelling an order to choose a different payment method failed after the order was already marked cancelled (e.g. a slow or failing third-party hook), the customer saw "Could not cancel your order" even though the cancellation had already gone through. The cancellation now always confirms success to the customer once the order is genuinely cancelled.
* feat: Re-added the "Payment Display Mode" setting (Modal vs Dedicated Page). It now automatically detects whether the store's Checkout page uses the WooCommerce Checkout Block and falls back to Modal mode with an on-screen notice in that case, since Dedicated Page mode only works with the classic [woocommerce_checkout] shortcode.

= 1.10.5 — 2026-07-07 =
* change: The expand/collapse indicator on each payment option in the M-Pesa payment-waiting screen (STK Push and Paybill/Till) now shows a clear "+" or "−" instead of a small rotating arrow, so it's obvious the section can be expanded.

= 1.10.4 — 2026-07-07 =
* change: Removed the "Payment Display Mode" setting. Testing found it only works on classic WooCommerce checkouts — stores using the WooCommerce Checkout Block would see a broken order-pay page with no way to complete payment. It will return once Blocks-based checkouts are supported.

= 1.10.3 — 2026-07-07 =
* fix: Dedicated Payment Page mode redirected to the correct URL but WooCommerce showed a bare order summary with no payment form, because the redirect was missing the `pay_for_order` flag WooCommerce requires to render the actual payment form on that page.

= 1.10.2 — 2026-07-07 =
* fix: "View order details" on the payment-waiting screen could keep showing a stale "cancelled" state even after the order was actually confirmed paid moments later, because it only hid the modal instead of refreshing the page. It now always reloads to show the order's true current status.
* fix: Dedicated Payment Page mode never displayed the payment screen on WooCommerce's pay-for-order page, because it hooked a WordPress action name that does not exist in WooCommerce core.

= 1.10.1 — 2026-07-07 =
* fix: "Choose another payment method" caused a fatal error and left the customer stuck on a broken page instead of cancelling the order, because the cart-restore step called a method that doesn't exist on WooCommerce's order item objects.

= 1.10.0 — 2026-07-06 =
* feat: Added a "Payment Display Mode" setting so the M-Pesa payment screen can be shown as the existing overlay modal, or on its own dedicated payment page instead.
* fix: The "Place Order" button now shows a clear "Preparing your M-Pesa payment…" status banner while the order is being submitted — previously WooCommerce's own loading overlay could hide the button's status text, leaving customers looking at a bare spinner with no explanation.
* fix: Customers no longer briefly see the plain "Your order has been received" page before the M-Pesa payment screen appears — the correct payment screen now shows immediately on load instead of flashing in after a delay.
* fix: "Choose another payment method" sometimes showed "Could not cancel your order. Please try again or contact support." even when the cancellation had actually worked, because the more specific message was never able to reach the customer. The correct message is now shown, and cancelling reliably returns the customer to checkout with their cart restored.
* fix: The manual payment option (Option 2) was faint, hard to read, and didn't look clickable next to the STK Push option. Both payment options now have equal visual weight, clearer borders, readable labels, and a visible hover state.

= 1.9.4 — 2026-07-06 =
* fix: Two M-Pesa decline reasons (transaction expired, system busy) were showing a generic "M-Pesa declined the payment" message instead of a specific, actionable one in order notes and the customer-facing failure screen.

= 1.9.3 — 2026-07-06 =
* fix: Orders where the customer never responded to the M-Pesa prompt and closed the browser could stay stuck as "Pending payment" forever, since Safaricom doesn't always send a callback for a silent timeout. A new background check now runs every 30 minutes, asks M-Pesa directly whether each such order was actually paid, and resolves it automatically — or flags it for a quick manual check if M-Pesa confirms payment but no receipt was ever received.

= 1.9.2 — 2026-07-06 =
* fix: The payment-waiting screen could get stuck showing "Waiting for payment confirmation…" indefinitely if the customer left the browser tab (e.g. to check their phone) and never returned before the M-Pesa prompt timed out. The countdown now uses real elapsed time instead of a tab-activity-dependent counter, and reconciles immediately when the tab becomes active again.

= 1.9.1 — 2026-07-06 =
* change: The M-Pesa payment-waiting screen is now a compact accordion — the STK Push and Paybill/Till options collapse into single-line headers you tap to expand, instead of both being shown in full at once.
* fix: The Paybill/Till panel no longer shows a transaction-code submission box that could never succeed on the Free edition; it now shows a note to contact support after paying manually instead.

= 1.9.0 — 2026-07-06 =
* feat: Customers waiting for an M-Pesa payment prompt can now cancel and return to checkout — with their cart restored — to choose a different payment method, instead of being stuck with no way back.

= 1.8.5 — 2026-07-02 =
* fix: The Settings page now shows the actual callback URL (including the security token) that gets registered with Safaricom, instead of a bare URL missing the token merchants need to copy for manual registration.

= 1.8.4 — 2026-07-02 =
* security: Every M-Pesa/Daraja callback URL (STK Push, C2B, Transaction Status) now carries a random per-site token that is verified on arrival, adding a second, environment-independent layer alongside the Safaricom IP whitelist. The token itself already existed on the STK callback URL but was never actually checked.

= 1.8.3 — 2026-07-02 =
* change: The manual-verification rate limiter was refactored internally to share its counting logic with a new reusable helper. No change in behaviour — the same attempt limits and lockout rules apply as before.

= 1.8.2 — 2026-07-02 =
* security: Manual M-Pesa code verification and the Transaction Status callback now reject a receipt or code that's already attached to a different order, closing a window where a colliding or forged code could confirm the wrong order as paid.
* security: Client-supplied IP headers (X-Forwarded-For, X-Real-IP, CF-Connecting-IP) are now only trusted when the request actually comes through a known reverse proxy or CDN, closing a bypass where an attacker could spoof their IP to defeat the manual-verification rate limiter.
* security: Manual payment verification attempts are now serialized per order and per IP, closing a race that allowed more than the intended number of verification attempts under concurrent submissions.
* security: Stored Daraja API credentials are now encrypted with AES-256-GCM (authenticated encryption) instead of AES-256-CBC, so tampering with the stored value is detected rather than silently producing corrupted plaintext. Existing stored credentials continue to work.

= 1.8.1 — 2026-07-01 =
* security: The STK Push callback concurrency lock is now backed by a dedicated, atomic database table instead of a WordPress transient, closing a window where two near-simultaneous Daraja callbacks for the same payment could both be processed as new.

= 1.8.0 — 2026-06-25 =
* feat: Telemetry now reports basic payment funnel metrics (STK initiated/confirmed/failed/retried, manual code submissions) to give aggregate product analytics for the fleet.
* fix: Telemetry error buffer was never cleared because the server returns HTTP 202 Accepted, not 200 OK. The response check now accepts any 2xx response.
* feat: New action hooks: ozupay_stk_initiated (on successful Daraja acceptance of STK push), ozupay_stk_retried (customer retry button click).

= 1.7.17 — 2026-06-25 =
* feat: M-Pesa QR code now appears below the order summary on the order-received (thank you) page for pending orders, visible outside the payment modal. Also appears on the order-pay page when the QR feature is enabled.

= 1.7.16 — 2026-06-25 =
* fix: QR code in the payment-waiting modal is now cached in order meta after the first generation. Subsequent page loads serve the cached QR instead of calling the Daraja API again. Cache is invalidated if the payable amount changes.

= 1.7.15 — 2026-06-25 =
* feat: M-Pesa QR Code now appears as OPTION 3 in the payment-waiting modal on the order-received page when the QR feature is enabled. The QR is generated server-side and embedded directly — customers can scan it immediately without any additional clicks.

= 1.7.14 — 2026-06-25 =
* fix: Manual Payment Verified email now renders with the WooCommerce email header and footer. Previously it sent raw HTML with no store branding.

= 1.7.13 — 2026-06-25 =
* fix: "OzuPay settings saved" admin notice now appears immediately after saving. Previously, add_action( 'admin_notices' ) was called inside render() which runs after the admin_notices hook has already fired, so the confirmation was never shown.

= 1.7.12 — 2026-06-25 =
* fix: "M-Pesa Payment Details" section no longer appears twice on the WooCommerce Blocks order-confirmation page. The compact gateway-level receipt block duplicated the loader's full receipt block because both were hooked to the same action. The redundant gateway hook and method have been removed.

= 1.7.11 — 2026-06-25 =
* fix: Transaction Status callback no longer crashes when Daraja sends an empty or non-JSON body — mirrors fix already applied in Pro.
* fix: Payment waiting modal now shows a failure state after 3 consecutive network errors during polling instead of silently spinning.
* fix: readme.txt Tested up to corrected from 7.0 to 6.8; changelog entries missing since 1.7.3 added.

= 1.7.10 — 2026-06-25 =
* security: Removed the X-OzuPay-Sandbox-Inject request header bypass that allowed any caller who knew the header name to skip the Safaricom IP whitelist check in production mode.

= 1.7.9 — 2026-06-25 =
* security: C2B callback no longer falls back to treating a numeric BillRefNumber as a WooCommerce order ID. Previously an attacker could confirm an arbitrary order by paying via Paybill with that order's ID as the BillRefNumber and the exact order total.
* security: STK callback now rejects a receipt number that has already been recorded on any order, not only on the order matched by CheckoutRequestID. Prevents a forged callback from confirming a second order using a previously used receipt.
* security: STK callback concurrency lock upgraded to a compare-and-swap pattern, preventing two simultaneous Daraja callbacks from both passing the initial transient check.

= 1.7.8 — 2026-06-23 =
* fix: Daraja OAuth token endpoint now validates JSON before accessing the response body, eliminating a PHP 8.x deprecation warning when Daraja returns a non-JSON response such as a WAF error page.

= 1.7.7 — 2026-06-23 =
* fix: STK Push, C2B, and Transaction Status callback handlers now catch all uncaught exceptions and return a valid Daraja response, preventing Safaricom from indefinitely retrying failed callbacks.
* fix: Daraja client now validates JSON in API responses and returns a structured error instead of silently propagating null into payment processing.

= 1.7.6 — 2026-06-22 =
* fix: The ozupay_manage_credentials capability is now restored automatically on the first admin page load if it was ever stripped.
* fix: OZUPAY_VERSION constant now correctly reads 1.7.6, matching the plugin header Version field.
* change: Plugin text domain changed from ozupay to ozupay-free to match the plugin slug, enabling correct translation loading.
* fix: Docs menu item now uses wp_safe_redirect() instead of wp_redirect() for safer redirect handling.
* fix: Payment Issue page URL parsing now uses WordPress wp_parse_url() wrapper instead of the native parse_url().

= 1.7.5 — 2026-06-22 =
* fix: Uninstalling with "remove all data" no longer drops shared database tables or deletes order meta when the Pro plugin is still installed on the same site.

= 1.7.4 — 2026-06-21 =
* change: Help FAB now shows "Contact Us" linking to ozupay.com/contact instead of "Support", reflecting that free users are directed to the public contact form.

= 1.7.3 — 2026-06-19 =
* change: Added translators comments before all translatable strings containing printf placeholders, as required by WordPress i18n standards.
* change: Converted unordered printf placeholders (%s, %s) to ordered form (%1$s, %2$s) so translators can reorder them.
* change: Removed deprecated load_plugin_textdomain() calls — WordPress 4.6+ loads text domains automatically.

= 1.7.2 — 2026-06-18 =
* fix: Countdown no longer skips intermediate values — a guard in startPolling() now clears any existing interval before starting a new one, preventing two intervals from racing.

= 1.7.1 — 2026-06-18 =
* fix: Countdown in the payment waiting modal no longer shows "0s" immediately after retrying.

= 1.7.0 — 2026-06-18 =
* feat: The payment waiting modal now displays a countdown showing how many seconds remain before checking stops.
* feat: The number of STK Push retry attempts is now configurable in the M-Pesa gateway settings (0–5 retries; default 2).

= 1.6.9 — 2026-06-18 =
* fix: After a payment failure, refreshing now correctly shows the red failed state immediately without blocking polling — allowing a subsequent Retry to work correctly on the same page load.

= 1.6.8 — 2026-06-18 =
* fix: After an STK Push failure, refreshing the order-received page no longer briefly flashes the "payment pending" green state before reverting to the red failed state. The failed state is now applied immediately on page load.

= 1.6.7 — 2026-06-18 =
* fix: Closing the payment waiting modal (via the X button or "View order details") now persists the dismissed state in sessionStorage, so the modal does not re-open if the page reloads immediately afterwards.

= 1.6.6 — 2026-06-18 =
* fix: Close button in the M-Pesa payment waiting modal increased from 30×30px to 44×44px to meet minimum touch target size on mobile.
* fix: Phone number input, Retry button, transaction code input, and Submit button in the waiting modal increased to 44px height for reliable tap accuracy on mobile.
* fix: On phones narrower than 480px the waiting modal now slides up from the bottom of the screen (bottom sheet) with extra bottom padding for home indicator clearance.

= 1.6.5 — 2026-06-18 =
* fix: Clicking "View order details" inside the M-Pesa waiting modal navigated to a URL that triggered the modal to re-open immediately. The button now closes the modal in place and shows the payment pending bar, matching the behaviour of the close button.

= 1.6.4 — 2026-06-18 =
* fix: The M-Pesa payment waiting modal briefly flashed on page load before JavaScript ran, because the backdrop element was visible by default in HTML. The backdrop now starts hidden and is shown explicitly by script.

= 1.6.3 — 2026-06-17 =
* fix: After a fresh checkout, the payment-waiting modal was never shown — customers always saw the pay prompt instead of the "Waiting for confirmation" modal because the stkJustSent flag was missing from the localised script data.
* fix: The STK retry AJAX endpoint was not registered, so the pay prompt's "Send Payment Request" button silently failed when clicked. The endpoint is now registered in both standard and guest request contexts.
* fix: "Sending…" and "Pay via M-Pesa" labels on the pay prompt were hardcoded in English; they are now translatable strings.

= 1.6.2 — 2026-06-17 =
* fix: After a successful M-Pesa STK Push, guest customers were redirected to the order-received page without the post-payment bypass marker, causing WooCommerce's "confirm your email" gate to appear instead of the order confirmation. The bypass marker is now included in all redirects from the payment flow and the gate is correctly skipped.
* fix: When the payment-waiting modal reloaded the page after confirming payment or after a deposit was confirmed, the reload URL lacked the bypass marker, causing the email verification gate to appear for orders placed more than 10 minutes ago.

= 1.6.1 — 2026-06-16 =
* fix: The new "M-Pesa Payment Details" section on the order-received page rendered twice on stores using the WooCommerce Cart & Checkout Blocks order-confirmation template, since two different blocks on that page both fire the same legacy hook it was attached to.

= 1.6.0 — 2026-06-16 =
* feat: M-Pesa payment details (receipt and phone) are now shown permanently on the order-received and My Account "View Order" pages, not just in the order confirmation email and the temporary payment status popup.

= 1.5.4 — 2026-06-16 =
* fix: After submitting a manual M-Pesa transaction code, guest customers were redirected to the My Account "View Order" page, which requires login and showed a sign-in wall instead of their order confirmation. The payment-waiting screen now stays on the order-received page, which works for guest checkouts.

= 1.5.3 — 2026-06-16 =
* change: At checkout, the M-Pesa phone field now fills in automatically from the billing phone number as you type, so you don't have to enter the same number twice. Editing the payment phone field directly stops the auto-fill, so paying from a different number still works.

= 1.5.2 — 2026-06-16 =
* change: Removed remaining decorative icon HTML entities from feature card calls (Sandbox Testing, Health Check, Transaction Log, COD Deposit) and the floppy disk emoji from the Save Settings button.

= 1.5.1 — 2026-06-15 =
* change: Removed all decorative emoji and icon HTML entities from admin UI — Pro upsell cards, admin menu label, danger warning text, gateway description, and order panel link.

= 1.5.0 — 2026-06-15 =
* feat: C2B Paybill fallback is now available in the free edition. When STK Push fails, customers can pay manually via Paybill and the payment is automatically matched to their order by account reference.
* feat: Settings page now shows all three Daraja callback URLs (STK, C2B confirmation, C2B validation) with copy buttons, so merchants can configure all required URLs in the Daraja portal at once.
* change: Pro features panel no longer lists C2B Buy Goods Fallback as a Pro-only feature, since it is now included in the free edition.
* change: Removed decorative icons from the Pro features panel cards.

= 1.4.23 — 2026-06-15 =
* feat: Pro features panel now lists Transaction Fee Passthrough, M-Pesa QR Code, and Payment Links alongside the existing Pro feature cards, giving users a complete picture of what is available in the Pro edition.

= 1.4.22 — 2026-06-15 =
* feat: Settings page now shows the STK callback URL for this installation with a one-click copy button, so users know exactly which URL to enter in the Daraja portal during go-live.

= 1.4.21 — 2026-06-14 =
* fix: Closing the payment modal while in the failed state no longer causes it to reappear automatically.

= 1.4.20 — 2026-06-14 =
* fix: Clicking "Pay" from the order-details page no longer immediately shows the previous payment's failure state. The stale failure flag is now cleared as soon as a new STK Push is successfully initiated.
* fix: A JavaScript safety net now ensures that if a fresh STK was just sent (order-pay redirect), any leftover failure flag from a prior attempt is ignored and polling starts immediately.
* feat: M-Pesa payment details (receipt/transaction code) are now displayed below the order summary on the order-confirmation and My Account → Orders view pages after a successful payment.

= 1.4.19 — 2026-06-11 =
* change: Help FAB trigger reduced to 26 px and menu pills made smaller. "Documentation" added as a third option alongside "Request a Feature" and "Support".

= 1.4.18 — 2026-06-11 =
* change: Floating help button replaced with a small "?" icon that expands into "Request a Feature" and "Support" links when clicked. Collapses when clicking outside.

= 1.4.17 — 2026-06-11 =
* change: Removed decorative emojis from admin pages (About, Settings, floating button, order panel). Replaced with plain text.
* change: "Unlock" replaced with plain language throughout admin copy.
* change: Settings page "Help Improve OzuPay" section renamed "Usage Data" with shorter, factual copy.
* change: Pro upgrade prompts reworded to state features directly rather than using marketing language.
* change: About page footer changed from "Built with ❤️ by OzuPay" to a plain link.

= 1.4.16 — 2026-06-11 =
* change: "Request a Feature" links now point to ozupay.com/feature-request instead of ozupay.com/support.

= 1.4.15 — 2026-06-11 =
* feat: "Request a Feature" floating button now appears on every OzuPay admin page (bottom-right corner), linking to ozupay.com/support.

= 1.4.14 — 2026-06-11 =
* feat: "Request a Feature" link added to the About page quick links, pointing to ozupay.com/support.

= 1.4.13 — 2026-06-10 =
* feat: New OzuPay → About admin page showing plugin version, PHP/WordPress/WooCommerce environment, and quick navigation links.
* feat: "Check for Updates" button on the About page triggers WordPress' built-in update check and redirects to the Updates page. If a newer version is available it will appear there.
* change: Plugin row on the Plugins page now shows an "About" link.

= 1.4.12 — 2026-06-10 =
* change: Telemetry reports are now sent to the OzuPay license server (ozupay.com) instead of the non-existent api.ozupay.com endpoint. Opt-in telemetry will now be received and displayed in the admin analytics dashboard.

= 1.4.11 — 2026-06-10 =
* change: The Place Order button now shows a spinner and "Sending payment request…" text immediately when clicked with M-Pesa selected, giving instant visual feedback during the Daraja API call. The button is restored automatically if checkout validation or the server returns an error.

= 1.4.10 — 2026-06-10 =
* fix: Reverted the deferred STK Push approach (1.4.8–1.4.9). The synchronous STK Push in process_payment() is restored so a Daraja failure keeps the cart intact and the customer on the checkout page.

= 1.4.9 — 2026-06-10 =
* fix: When Daraja is unavailable and the deferred STK Push fails, the modal now shows a "Try another payment method" link back to the order payment page.

= 1.4.8 — 2026-06-10 =
* change: The M-Pesa STK Push is now sent from the browser after the order-received page loads, rather than during checkout processing. The payment modal appears immediately on redirect; the push arrives on the customer's phone ~1 second later, eliminating the checkout spinner delay.

= 1.4.7 — 2026-06-10 =
* fix: PHP 8.1 deprecation notice "Calling static trait method OzuPay_Gateway_Shared::mpesa_int is deprecated" — amount conversion is now a standalone ozupay_mpesa_int() function used by all Daraja and callback handlers.

= 1.4.6 — 2026-06-05 =
* fix: "Get OzuPay Pro" green link now actually appears on the Plugins screen — the plugin_action_links hook was missing from the file despite being listed in the 1.4.5 changelog.

= 1.4.5 — 2026-06-04 =
* feat: "Get Pro" link in green added to the plugin row on the WordPress Plugins screen, linking to ozupay.com/pro.

= 1.4.4 — 2026-06-04 =
* feat: Sandbox shortcode notice added below Business Shortcode — shows Daraja test shortcode 174379 with a "Use this" button that copies the value and sets Shortcode Type to Paybill. Only visible when environment is Sandbox.
* change: Sandbox passkey notice restyled to compact pill (title + truncated key left, button right, max-width 25em) matching Pro version.

= 1.4.3 — 2026-06-04 =
* fix: Settings page no longer clears all fields on save. The sticky Save Settings bar sits outside the <form> tag, so submitting via $form[0].submit() never included the ozupay_save_settings trigger in the POST data — save() was never called. Added a hidden input inside the form so the save always fires.

= 1.4.2 — 2026-06-04 =
* fix: STK callback now acquires a 30-second transient lock per CheckoutRequestID before processing. Concurrent Daraja retries arriving within milliseconds of each other could previously both pass the idempotency guard and double-confirm the same payment.
* fix: STK success callbacks with a zero amount or missing receipt are now rejected with a security log entry. A malformed success callback could otherwise falsely mark an order as paid with no money received.
* fix: IP-blocked callbacks now respond with wp_send_json() instead of http_response_code(403)+exit, ensuring WordPress shutdown hooks run cleanly.
* change: mpesa_int() docblock corrected — the function rounds (not ceils) to the nearest shilling. Code was always correct; only the comment was wrong.

= 1.4.0 — 2026-06-04 =
* fix: M-Pesa amounts enforced as integers throughout — new mpesa_int() helper ceils any float to the nearest KES integer. Used in process_payment, STK callback validation, and status callback.
* fix: STK callback amount_paid cast from float to int — M-Pesa always returns integer KES values.
* fix: Expected amount in underpayment check now uses mpesa_int() so the comparison matches what was actually sent to Daraja.
* change: Sandbox testing UI redesigned — light cards replacing dark backgrounds, colour-coded section borders, improved form inputs.
* change: Sandbox amount inputs now enforce step=1 to prevent decimal entry.
* change: Settings page credential fields visible while editing, masked after blur/save.
* feat: Sandbox universal passkey notice with one-click fill when Sandbox environment is selected.
* feat: Sticky save button at the bottom of the Settings page — always visible without scrolling.
* fix: Dark backgrounds removed from Settings page feature cards and banners.

= 1.3.4 — 2026-06-02 =
* fix: Payment Issue page (shown when STK fails) now shows correct manual payment steps for Till and Paybill. Till: no account reference, Buy Goods steps. Paybill: Business Number + Account Number with a warning to type it exactly. Both show a summary box with the key numbers.
* fix: Manual payment instructions email is now till-aware. Previously always showed paybill text. Email class now uses the HTML template via get_content_html() instead of building inline text that bypassed the template.
* fix: Payment Issue page and instructions email now respect the customer_number setting.

= 1.3.3 — 2026-06-01 =
* fix: Phone number input in the payment modal now keeps its correct styling on themes that declare a high-specificity input rule — the CSS selector is now scoped to the modal wrapper ID.

= 1.3.2 — 2026-06-01 =
* change: Removed dead deep-link JavaScript code from the payment modal JS file (the feature was always disabled in the free edition and the dead code had no effect on behaviour).

= 1.3.1 — 2026-05-31 =
* fix: Payment modal stuck permanently on "Waiting for payment confirmation" even after payment was completed. Root cause: the shared JavaScript file references cfg.i18n.retriesLeft.replace(...) during initialisation. This string was absent from the free edition's wp_localize_script call, causing a TypeError that halted execution before startPolling() was ever called. Added all nine missing i18n strings so the polling loop starts correctly and detects payment confirmation within 3 seconds of the Daraja callback.

= 1.3.0 — 2026-05-31 =
* feat: Settings page now includes a "Danger Zone" section with two uninstall modes — Keep Data (default) and Remove Everything. The chosen mode is saved so uninstall.php can honour it at deletion time.
* feat: Uninstall handler added. Keep mode clears cron jobs only. Remove Everything drops all three custom tables, deletes all plugin options and transients, removes custom capabilities, and deletes all _ozupay_* order meta (both post meta and HPOS wc_orders_meta).
* change: The ozupay_uninstall_mode option key is shared with the Pro edition for seamless upgrade compatibility.

= 1.2.0 — 2026-05-31 =
* fix: Excess headspace at top of payment modal — margin-top:0 !important resets and reduced sheet top padding.
* fix: Close button white/black theme override — high-specificity selector + !important forces the solid red background and white × that the theme was overriding.

= 1.1.9 — 2026-05-31 =
* fix: WooCommerce price colour override inside payment modal — amounts no longer appear in red. Custom instructions forced to dark text. STK messages shortened to single lines.

= 1.1.8 — 2026-05-31 =
* change: Payment modal compacted — all content now fits without scrolling on a standard mobile viewport. Same spacing and text reductions as the Pro edition.

= 1.1.7 — 2026-05-31 =
* fix: Deposit summary text on the checkout page had no explicit color and inherited an unreadable light value from some themes. Added color: #111827 / #374151 to the deposit table rows so the text is always dark on the light green background.

= 1.1.6 — 2026-05-31 =
* fix: Activating the free edition while OzuPay Pro is already active now shows a clear error message ("OzuPay Pro is already active — there is no need to install the free edition") and aborts the activation instead of silently creating a conflict.

= 1.1.5 — 2026-05-31 =
* fix: PHP warnings "Constant already defined" — all define() calls now use defined() || define() guards. Also added class_exists() guards in ozupay_free_activate() to prevent a fatal if classes are already loaded when the activation hook fires.

= 1.1.4 — 2026-05-31 =
* fix: Global functions (ozupay_load_gateway_classes, OzuPay) are now wrapped in function_exists() guards to prevent a PHP fatal error if both the free and Pro editions are included by WordPress simultaneously. Activation and deactivation hooks renamed to ozupay_free_activate / ozupay_free_deactivate to avoid collision with Pro edition function names.

= 1.1.3 — 2026-05-31 =
* change: "Pro" badge on feature cards changed to solid amber (#D97706) with white text for legibility on white card backgrounds.

= 1.1.2 — 2026-05-31 =
* change: Settings page — "Help Improve OzuPay" telemetry section now appears after the Features grid, not before.
* change: Telemetry banner updated from amber gradient to dark navy with green border, matching the Pro edition style. Text updated to white/near-white for contrast.
* change: Telemetry opt-in checkbox now has a white background with a green border when unchecked, making it clearly visible on dark surfaces.
* change: Pro upgrade banner changed from indigo gradient to dark navy with a green left accent and green button. Replaces the amber call-to-action button.
* change: "Pro" feature badge changed from purple gradient to gold (#F59E0B) to match the Pro edition premium badge style.
* change: "Free" feature badge changed from green gradient to flat green tint to match the single-accent design approach.
* change: Sandbox Testing panel intro text changed to black. "Get sandbox credentials" link changed from indigo to green.
* change: Sandbox Testing panel buttons (indigo, green, purple, amber) unified to a single M-Pesa green (#00A651).

= 1.1.1 — 2026-05-31 =
* fix: If both the free and Pro editions are simultaneously active, the free edition now detects the conflict on load, shows an admin notice, and halts its bootstrap — preventing duplicate gateway registration and hook conflicts.

= 1.1.0 — 2026-05-31 =
* feat: Optional plugin telemetry — site owners can now opt in to share non-sensitive usage data (site URL, version, environment) to help improve OzuPay development. No customer data is collected. Telemetry is disabled by default and can be enabled in Settings.

= 1.0.0 — 2026-05-31 =
* feat: Initial release of OzuPay M-Pesa Payments free edition with STK Push gateway, payment waiting modal, manual verification fallback, transaction log, sandbox testing panel, and health check.
