Donor Merchant changelog: every release before the current one.
The current release is in readme.txt. Plain-language summaries: https://www.donormerchant.com/changelog/

= 2.9.0 =
* New: donors with a recurring PayPal gift can cancel it from the donor portal.
* New: a "Recurring payment failed" email asks the donor to update their card when a renewal is declined. Add the new webhook events listed on the Stripe and PayPal screens.
* New: campaign end dates. The form closes itself after the date.
* New: "Mark received" asks for the date the money arrived, so the gift lands on the right year's statement, and how it came in (check, cash, bank transfer or your own methods such as Cash App) with an optional reference such as a check number.
* New: a Funds card on the Dashboard with totals for each fund, and donations can be searched by fund.
* Improved: recurring renewals keep the donor's answers to your custom form fields.
* Improved: the Donations list shows when a recurring gift has been cancelled.

= 2.8.0 =
* New: Gift Aid for UK charities. Collect HMRC's declaration on the form and download your claim in HMRC's format, under Donor Merchant > Gift Aid.
* New: an optional employer matching question, shown on the donation, your new-donation email and the receipt.
* Improved: partial Stripe refunds are recorded on the donation.

= 2.7.2 =
* Security: hardening, update recommended.
* Improved: a faster Dashboard and complete CSV exports on sites with many donations.
* Fix: row actions such as "Mark received" return to the list instead of an empty page.
* Fix: receipts and annual statements use the date a gift was received.
* Fix: custom wording no longer changes email placeholders such as {organization}.
* Fix: suggested amounts and fee settings on sites that use a decimal comma.
* Fix: recurring gifts keep working after switching to a different Stripe or PayPal account.
* Fix: smaller fixes to emails, backdated gifts and the setup checklist.

= 2.7.1 =
* Improved: the Dashboard chart shows a bar for each day, an amount scale, and the date, amount and number of gifts when you hover a bar.
* Fix: a hidden chart caption could show on top of the Dashboard chart in some browsers.

= 2.7.0 =
* New: email customization, under Settings, Email. Each email the plugin sends is listed with its status and recipients, and has its own screen to switch it on or off, change the subject, heading and message, choose HTML or plain text, and add or remove the donation details table. New-donation alerts and the monthly summary can go to several addresses.
* New: an email template with your logo, header alignment, font, footer text and five colors. The accent and button shape start out matching your donation form.
* New: a live preview of every email on desktop or mobile, and a "Send a test email" button, both using your unsaved changes.
* New: a separate receipt for recurring gift renewals ("Thank you for your continued support"), so a donor's third monthly receipt no longer reads like their first. It starts switched on if your receipts were on.
* New: "From" name, "From" address and "Reply-to" address settings. Replying to a new-donation alert now reaches the donor.
* Changed: receipts now show the gift, any processing fee covered, frequency, date, designation, campaign and tribute in a table, with a button for the printable receipt. If you had edited your receipt message, it is kept exactly and the table starts switched off, so nothing is listed twice.
* Changed: the new-donation alert, portal sign-in link and monthly summary now use the same template. Existing settings for the alert and summary carry over.
* Changed: the "Send me a sample now" button for the monthly summary is replaced by the preview and test email on its screen.
* New: a Payments tab like WooCommerce's: Stripe, PayPal, bank debit (ACH) and offline gifts listed with their status, each with its own settings screen. Fee recovery and the card and bank fee figures have their own Processing fees screen there, and suggested amounts, the minimum, recurring giving and giving frequencies moved from General to the Forms tab.
* New: offline gifts. Donors can choose to give by check, bank transfer or cash; you set the name and instructions. The gift is saved as "Awaiting payment", the donor is emailed your instructions, and it is never counted or cleared away until you use "Mark received", which sends the receipt. One-time gifts only.
* New: European bank payments through Stripe, such as iDEAL, Bancontact, EPS and Przelewy24, for one-time gifts. Switch on "Bank redirect methods" on the Stripe screen and the methods you want in your Stripe dashboard. Donors return to your form after approving at their bank.
* New: Venmo with the PayPal buttons for US donors paying in US dollars (one-time gifts), on the PayPal screen.
* New: register your domain for Apple Pay with one button on the Stripe screen, instead of in the Stripe dashboard.
* New: an "Offline gift instructions" email, customizable like the others.
* New: a fuller Dashboard: pick a period (7, 30 or 90 days, 12 months or all time) and see what was raised, donations, average gift, new donors and fees covered, each compared with the period before, plus a chart, top campaigns, top donors, how donors gave, what needs attention (offline gifts awaiting payment, bank payments processing, failed payments, refunds) and a setup checklist for new sites that you can dismiss.
* Improved: the Import screen is shorter, with the supported platforms shown as a checklist.
* Fix: on the Campaigns screen, long shortcodes ran past the edge of the table. Each now fits and has a Copy button.

= 2.6.0 =
* New: weekly and every-2-weeks recurring donations, with Stripe and PayPal.
* New: choose which giving frequencies your form offers, under Settings, General, Giving frequencies. Existing sites keep exactly the frequencies they had.
* New: choose the words donors see. Switch "donation" and "donate" to "gift" and "give", or to your own words, on the form, donor portal, receipts, statements and emails. Your admin screens keep the standard wording. When you change the wording, the PayPal button shows the PayPal logo instead of its own "Donate" label, which PayPal does not let sites change.
* New: a custom wording table, under Settings, Forms, Wording, to replace any other text donors see with your own.
* Changed: number formatting now works like WooCommerce's. Under Settings, General, set the currency position (left, right, or either with a space), the thousand separator (or none), the decimal separator, and the number of decimals. Fewer decimals only drop zeros, so a $1.80 fee is never shown as a rounded $2. The "Site language" choice and the preview are gone; an upgraded site keeps exactly the format it had until you change it.
* Improved: fee recovery now asks for the exact amount that leaves your full gift after the processor's cut, including its cut of the fee itself, so the form no longer says "about". On a $100 gift by card at 2.9% + 30 cents that is $3.30, not $3.20.
* Changed: a donation request for a giving frequency your form does not offer is now refused, instead of being quietly charged as a one-time gift.
* Changed: if your site uses page caching, clear the cache after changing the giving frequencies, so donors are not offered a choice you removed.
* Improved: the importer reads weekly and every-2-weeks schedules from GiveWP, Donorbox, PayPal and spreadsheet exports, and no longer mistakes values such as "Weekend Gala" or "Every 3 weeks" for weekly giving.
* Fix: the settings screens never showed "Settings saved." or their warnings. Both now appear.
* Fix: an amount typed with a third decimal place, such as 5.225, could show a total a cent different from the charge. The form now rounds amounts exactly as the payment is rounded.

= 2.5.5 =
**Importing**

* Fix: a donation split across several campaigns in a Charitable export now imports every campaign's share. The preview always matches what the import writes, and re-importing never duplicates earlier imports, even after a campaign was renamed.
* Fix: pressing Import twice, or again while a large file is still importing, no longer imports donations twice.
* Fix: imported dates keep their time and timezone, and day-first dates such as 26/09/2026 or translated month names such as "26 septembre 2026" import on the right day. Dates written with dashes or dots keep reading day first, and you can set the date order on the mapping screen. Two same-size gifts on the same day are no longer merged into one.
* Fix: pre-approved, abandoned, declined and voided donations no longer import as completed gifts, partially refunded ones stay in your totals, and refund lines with negative amounts are reported instead of imported. The preview now lists any status it does not recognize instead of quietly counting those gifts as completed.
* Fix: after a preview, the import now runs with exactly the column mapping you previewed, rather than the automatic guesses.

**PayPal**

* Fix: refunds and reversals of PayPal recurring payments were not recorded. A full refund or reversal now marks the payment refunded, and partial refunds are added up until they cover the whole gift.
* Fix: PayPal renewals recorded the previous payment's amount instead of what PayPal actually collected.
* Fix: a PayPal recurring gift whose first payment notice arrived before its activation notice was never recorded. It is now connected and completed, with the same pricing check as before.
* Fix: a PayPal donor whose payment was still clearing, or whose return to your site failed after approving, was shown an error inviting them to give again. They now see a thank-you that explains the payment is clearing.
* Fix: a PayPal refund that arrived before its payment had been recorded was ignored. It is now held and applied as soon as the payment is recorded.
* The PayPal webhook instructions now list PAYMENT.SALE.REFUNDED and PAYMENT.SALE.REVERSED. Add both to your PayPal webhook so recurring refunds are recorded.

**Fee recovery**

* New: when a donor covering fees pays a one-time gift by bank account (ACH), they are now asked to cover the bank fee instead of the card fee. Set the bank fee percentage and cap under General; the defaults match Stripe's standard US price of 0.8%, capped at $5.00. Recurring gifts keep the card estimate, because their price is fixed before the donor picks a payment method.

**Donor portal and caching**

* Fix: the donor portal now opts out of page caches and Cloudflare APO before the theme starts output, rather than from inside the shortcode where the headers were often too late.
* Fix: on a cached portal page the sign-in form could say a link was on its way and send nothing. The form no longer depends on a nonce, and when a request is refused it now says so.
* Fix: LiteSpeed Cache, which caches REST requests by default, could store receipts and annual statements, so a refunded gift's receipt could still show as valid.

**Settings**

* Fix: the Payments tab said Stripe was "Connected" when the saved key was a placeholder or the wrong kind for the mode. Keys that look wrong now show "Check keys" with a one-line reason, and the field itself is flagged.
* Fix: every save of the Payments tab erased two unused PayPal merchant ID settings.
* Fix: if the minimum donation setting was missing, the form accepted gifts from 1 rather than the 5 a new install starts with.
* Fix: creating a campaign without a status raised a PHP warning and a database error.
* The Minimum donation field now shows the minimum actually enforced. A saved 0 has always meant a floor of 1, and now reads that way.

**Reports**

* Fix: the monthly summary counted a gift in the month it was started rather than the month it completed, so a bank payment started near the end of a month and settled in the next appeared in neither summary.

= 2.5.4 =
* Fix: removing a fund took two clicks. Pressing the remove button moved focus out of the text field, which rebuilt the tag list and took away the very button being pressed before the click landed.
* Fix: the cross on a fund's remove button sat high inside its circle. It is now drawn rather than typed, so it is centred whatever font the admin screen uses.
* New: a Number format setting on the General tab, with a preview that updates as you change it so you can see exactly what donors will get before saving. Choose whether amounts use a point or a comma for decimals, and a comma, point, space or nothing for thousands, or leave both to follow your site language. It changes every amount donors see: the form, receipts, statements and your own reports.
* Fix: the donation form punctuated amounts differently from receipts and reports on any site whose language does not use English conventions. The form assumed a comma for thousands and a point for decimals; it now uses the same format as everything else.
* The minimum donation field shows the amount to your currency's precision, so 1 reads as 1.00, and nothing at all for currencies without a minor unit such as yen.
* The minimum donation field now shows your currency symbol beside it, and the symbol changes as soon as you pick a different currency rather than waiting for a save.
* Removed the up and down arrows from the minimum donation field. Stepping a money amount by one penny at a time was not useful.
* Fee percentage and Fixed fee are now shown as settings belonging to Fee recovery: indented under it, and dimmed while it is switched off, since they have no effect until it is on.
* The fund options help text no longer explains the designation dropdown twice, and the line under the field now says what donors will see whether or not any funds are set.
* Fix: on the Forms tab, the fund options box drew a second border inside itself, so the field looked like a box within a box.
* Fix: in the Custom fields table, the Required checkbox and the remove control sat above the line of the fields beside them instead of level with them.
* Fix: importing a Charitable donation export needed columns mapped by hand. Charitable's "Campaign Title", "Date of Donation" and "Donation Gateway" columns are now detected automatically, so gifts land in the right campaign and its thermometer starts at the right total.
* New: the importer skips donations made in test mode. Map a test-mode column (Charitable's "Made in Test Mode" is detected automatically) and any row marked yes is left out, so test gifts no longer count toward campaign totals or reports. The preview says how many were skipped.
* Fix: Stripe renewals lost the gift's fund designation from the second payment on, so recurring gifts to a restricted fund showed no fund. Renewals now carry it, as PayPal renewals already did.
* Fix: refunds of Stripe renewals were not recorded on accounts using Stripe's API version 2025-03-31.basil or later. Those renewals were saved under their invoice, which a refund never names. They are now saved under their payment, so a refund finds them, and a repeated notification is still recognised.
* Fix: when a PayPal recurring gift was connected only by PayPal's notification (the donor closed the window before returning to your site), its first payment was recorded as a second gift and the original stayed pending for ever. The first payment now completes the original gift. The same pricing check the site already makes on return from PayPal is now made here too.
* Fix: the Shortcodes settings tab showed a Save Changes button, which led to a WordPress error page because there is nothing on that tab to save. The button is gone.
* Fix: in the Number format preview, choosing "Site language" showed whatever format was last saved rather than your site language's, so the preview could disagree with what saving would do.
* Fix: the monthly summary email's "New donors" figure counted every donor record created that month, including abandoned checkouts, card-testing attempts, imported history and donors added by hand, so a fraud wave could read as a surge of new supporters. It now counts people whose first completed gift was that month.
* Fix: the [donor_merchant_progress] thermometer for a campaign with no goal showed an empty box that looked like a blank bar with no numbers. Visitors now see nothing until a goal is set, and administrators see a short note with a link to set one.

= 2.5.3 =
**Fixes**

* Fix: the "Add donation" button on the Donations screen led to "Sorry, you are not allowed to access this page." The screen was registered and then immediately removed from the menu to keep it hidden, but WordPress works out which screen to load by looking the page up in the menu, so removing it left the screen unreachable. It is now hidden after the page has loaded instead, and the screen opens normally.
* Fix: rate limiting grouped IPv6 donors by the first half of their address. On servers that report IPv4 visitors in IPv6 form, which is common, that half is all zeroes, so every donor on the site shared a single allowance and legitimate gifts were turned away once it ran out. Those addresses are now recognised and counted individually.
* Fix: the rate limit window kept moving. Each allowed request pushed the expiry back another hour, so instead of allowing 15 attempts per hour it allowed 15 in total and then refused everything until a full hour had passed with no activity at all. The window is now a real hour. This was felt most by donors sharing an address, such as a church office, a school, or a mobile network.

**New**

* New: add a donor by hand from the Donors screen, for a pledge, a prospect, or a record you are moving across yourself. If the email address is already on file, that donor is updated rather than duplicated.
* New: shortcodes have their own Settings tab, listing each one with a usage example, a copy button, and your campaign IDs so you can copy a shortcode that already works on your site.
* New: abandoned donation attempts are cleaned up automatically. Every started donation records a row before the payment is attempted, and forms that were never completed used to stay forever. Attempts still unfinished or failed after 90 days are now removed once a day. Completed and refunded records are never touched, and neither is anything attached to a recurring gift.

**Payments**

* Stripe: your donor's name and email address are now passed to Stripe with the payment, which improves Stripe's own fraud scoring, and the card form no longer asks for them a second time.

**Spam and abuse**

* New installs now default to a five dollar minimum donation rather than one dollar. Card testers favour the smallest amount a form will accept, and card networks advise setting a floor appropriate to real donors. Existing sites keep whatever minimum they already set.
* The donation form now rejects submissions completed impossibly fast, which catches simple automated scripts. It never rejects a slow one, so page caching is unaffected.

**For developers**

* New filter `donor_merchant_pre_donation` rejects a donation before anything is written or any payment is attempted, so a site that needs its own spam or fraud check, including a CAPTCHA, can add one without modifying the plugin.
* New filter `donor_merchant_client_ip` supplies the real visitor address on sites behind a proxy or CDN, where every donor would otherwise be counted as the same visitor.
* New filters `donor_merchant_min_fill_seconds` and `donor_merchant_stale_donation_days` tune or switch off the two behaviours above.

**Import**

* The importer now names the exports it recognises, including GiveWP, Charitable, Donorbox, PayPal, Stripe, WooCommerce, Gravity Forms and WPForms, instead of leading with a row limit.
* The row limit per file is raised to 10,000 and is only mentioned if your file is larger, in which case it tells you how many rows it found and what to do next.

**Also in this release**

* New: donations can be corrected from the Donations screen. "Mark completed", "Mark refunded" and "Mark failed" appear next to each record, so a gift with the wrong status no longer has to be deleted to be fixed. Correcting a record never emails the donor or fires integrations.
* Fix: a PayPal recurring donation is now recorded as completed when its first payment actually settles, not when the donor approves the agreement. Approval only authorises future billing, so a receipt could previously be sent for money that was never collected. Donors see a short "we will email your receipt when the first payment clears" message instead.
* Fix: donor records left behind by donation forms nobody completed are now cleaned up with the attempt itself. Donors you added by hand, and donors brought in by the importer, are never removed.

**Payments and records**

* Fix: Stripe subscription renewals were never recorded on newer Stripe accounts. Stripe moved where an invoice reports its subscription in its 2025-03-31 API version, and the plugin only read the old location, so from the second month onward a recurring gift was charged and no record appeared. Renewals are also now deduplicated by invoice rather than by payment intent, and take their amount from the invoice, so a gift the donor has since changed records the amount actually charged.
* Fix: a partial refund marked the whole donation refunded, removing it from your totals, the campaign thermometer, the donor's lifetime giving, and their annual tax statement. Only a full refund changes the status now, on both Stripe and PayPal.
* Fix: a refunded donation could be flipped back to completed by a redelivered webhook, sending a second thank-you for money that had been returned.
* Fix: PayPal gifts paid from a bank account (eCheck), or held by PayPal for review, stayed pending for ever. The plugin now handles PayPal's capture, refund and reversal events, so those gifts settle when the funds clear.
* Fix: a PayPal recurring donation is now attached to its record from PayPal's own notification. Previously this depended on the donor's browser completing one last request, and if that was interrupted PayPal kept billing while the site recorded nothing.
* Fix: bank (ACH) donations are recorded as processing while the money moves, instead of sitting at pending until a webhook arrived. Sites without a webhook configured left those gifts unrecorded indefinitely.
* Fix: if a donation could not be saved, the payment is no longer attempted. Previously the card could be charged with nothing on file to match it to.
* Fix: donation amounts in currencies without decimal places, such as JPY, are no longer stored with fractions that were never charged.
* Security: the PayPal activation endpoint no longer reveals whether a donation exists or has completed before the caller proves the subscription belongs to it.
* Security: the plugin no longer ships an unused contact endpoint that could send mail.

**Donor records**

* Fix: importing a CSV without name or phone columns erased those details on every donor it matched. Blank values can no longer overwrite what is already on file, on any screen.
* Fix: the importer treated a transaction ID as advisory, so two genuine same-day gifts of the same amount were merged into one even when each had its own ID. It also read European amounts incorrectly, importing "50,00" as 5000.00.
* Fix: the importer now tells you when a file is too large to carry between the two steps, instead of returning silently to the upload screen.

**Donation form**

* Fix: changing the amount or frequency while a payment was being prepared could charge the previous choice, including enrolling a donor in a recurring gift after they had switched back to one-time.
* Fix: a successful payment whose confirmation request failed showed the donor an error under a live Donate button, inviting a second gift. They now see the thank-you, and the webhook reconciles the record.
* Fix: changing the gift at the wrong moment could leave the Donate button permanently disabled, or show a technical error to a donor who had already paid.
* Fix: PayPal errors now show PayPal's own explanation rather than a generic message, and cancelling the PayPal window returns the form to normal.
* Accessibility: form messages are announced to screen readers reliably, and focus moves to a validation error rather than leaving the reader on the button.

**Other**

* The donor portal now shows only recurring gifts that have actually been paid, is marked as uncacheable so a page cache cannot serve one donor's history to another, and picks up your chosen accent color and button shape.
* Annual statements covering more than one currency no longer show a combined total that matched none of the gifts listed.
* A refunded donation's receipt link is now clearly marked as refunded rather than serving a full-value receipt.
* The monthly summary is sent even if the site had no traffic on the 1st, rather than skipping that month entirely.
* On multisite, choosing to delete data on uninstall now clears every site, and removes the plugin's temporary data as well.

**Forms**

* Changed: the Button shape setting now also shapes the giving frequency, donation amount, and payment method buttons, not just the donate button. Choosing Sharp used to produce a square donate button underneath fully rounded amount buttons. **On existing sites the amount buttons will look slightly less rounded after updating**, because they now follow the Rounded setting instead of always being fully round. Choose Pill to keep the previous look.
* Fix: on the Forms tab, the ring marking the selected accent color stayed on whichever color was saved and never moved when you picked a different one. Picking a color also cleared the ring entirely.
* Fix: the live preview on the Forms tab ignored your saved settings until you changed something, so it always opened showing the default blue and the default button shape.
* Changed: fund options are now entered one at a time and shown as removable tags, rather than as free text in a box asking for one per line.

**Settings**

* Settings: Stripe and PayPal are now clearly separate panels on the Payments tab, each with its own heading and a badge showing whether it is connected, so it is obvious which gateway a field belongs to. The Email and Integrations tabs are organised the same way, with one panel per outgoing email and per integration.
* Settings: each key field now shows the format it expects, such as `pk_live_...` or `whsec_...`, so a secret key cannot be pasted into a publishable key field by mistake.
* Settings: clicking a shortcode anywhere in Settings now selects the whole thing, including the brackets, ready to copy.
* Settings: added hoverable help icons to the fields that most often cause confusion, including the webhook secrets, the fee estimate values, the minimum donation amount, and the tax ID.
* Settings: the uninstall option now shows a plain warning describing exactly what is deleted, that deactivating the plugin does not trigger it, and that you should export your records first.
* Settings: corrected the Advanced tab, which still referred to a legacy shortcode that was removed in 2.4.0.
* Settings: tidied the Forms tab wording, switched to US spelling, and removed the hyphens from the thank you page label and its dropdown.
* Settings: the custom fields table now starts with one empty row, so it is clear you can type into it, and each row's remove control is a compact icon rather than a text link.

= 2.5.2 =
* Fix: in the donation form, a dropdown rendered a few pixels taller than the text field beside it in the same row, because inputs and selects were using different line heights. All form controls now share one line height and match exactly.
* The optional dedication section now uses a solid border rather than a dashed one, which reads as a real part of the form instead of a placeholder, and its heading and field labels have been shortened.

= 2.5.1 =
* Security: the public donation endpoint no longer reuses a donor's stored Stripe customer. Every recurring checkout creates its own customer, so knowing a donor's email address cannot attach a subscription to their Stripe record.
* Security: PayPal subscription activation now verifies the subscription uses the plan this site priced for that donation, so a cheaper plan cannot settle a larger donation record.
* Security: an existing donor's name and phone can no longer be rewritten through the public donation endpoint. Blank fields are still filled in.
* Security: Stripe webhook events are bound to this install and to the stored payment reference, so an event from another site sharing the same Stripe account cannot complete a local donation.
* Fix: a PayPal order is only recorded as paid when the capture itself is COMPLETED. A PENDING capture (eCheck or review) now stays pending until it settles.
* Fix: donation completion is a single atomic update, so simultaneous verify and webhook calls can no longer send duplicate receipts or fire integrations twice.
* Fix: the fee recovery line showed "$NaN" instead of the estimated fee. Numeric settings passed to the front end arrive as strings, which turned the fee calculation into string concatenation; they are now coerced to numbers and fall back to zero if a setting is empty or invalid.
* Fix: in the donation form, fields inside the optional dedication section could overflow their column and overlap the field beside them. The form set box-sizing by inheritance, and that chain did not hold inside the collapsible section, so those controls added their padding on top of their width. Box sizing is now stated outright.
* Fix: dropdowns in the donation form now match the height and width of the text fields beside them instead of keeping their browser default appearance.
* Hardening: rate limiting groups IPv6 clients by /64 rather than by single address, and the PayPal capture and activate endpoints are now throttled.

= 2.5.0 =
* New: Import donation history from another plugin or platform. Upload a CSV exported from GiveWP, Charitable, Donorbox, Givebutter, or a spreadsheet; columns are detected automatically and can be corrected before anything is written. Preview the result first, and re-run safely: gifts already on file are detected by transaction ID, or by matching donor, amount, and date.
* Imported donations are written silently. Historical records never trigger receipt emails, admin notifications, outgoing webhooks, or Mailchimp syncs.

= 2.4.1 =
* Sanitization: every register_setting() call now declares an explicit sanitize_callback. Secret fields use sanitize_text_field and preserve their stored value on a blank save through a pre_update_option filter (WordPress.org review follow-up).

= 2.4.0 =
* Form style customization: accent colour swatches with a live preview, plus a button-shape control (pill / rounded / sharp).
* Contact form REST endpoint for the marketing site (rate-limited, honeypot-protected).
* Security: payment secret keys are no longer echoed back into the settings page; secret fields render blank and keep their stored value on save.
* Compliance: all admin scripts and the receipt stylesheet are now loaded through the enqueue API; all custom option, transient, and shortcode names use the donor_merchant prefix; every database query is bound through wpdb::prepare() with the %i identifier placeholder.
* Requires at least: WordPress 6.2 (for the %i placeholder in wpdb::prepare()).

= 2.3.1 =
* Security: CSV exports now neutralize spreadsheet formula injection (donor-supplied values beginning with =, +, -, or @ are escaped).
* Security: PayPal subscription activation now strictly requires the subscription's custom_id to match the donation, removing an edge-case bypass.

= 2.3.0 =
* New: Monthly fundraising summary email - totals, new donors, recurring count, and top campaign, sent on the first of each month (with a "send a sample now" button).
* Docs: documented the developer hooks (actions, filters) and the signed outgoing-webhook payload.

= 2.2.0 =
* New: Bank debit (ACH) payments through Stripe - lower fees than cards.
* New: Recurring PayPal donations via PayPal Subscriptions, with a webhook for renewals.
* New: Designations / funds - donors choose which fund their gift supports.
* New: Integrations - outgoing webhooks (Zapier/Make) signed with HMAC, and Mailchimp audience sync with an opt-in checkbox.
* New: Custom form fields (text, paragraph, dropdown, checkbox), saved with each donation and included in exports.
* Improved: Donations CSV now includes designation and custom-field columns; admin notifications list them too.
* Developer: webhook payload includes campaign, designation, and custom fields.

= 2.1.0 =
* New: Campaigns - multiple donation forms, each with its own goal, suggested amounts, and description, plus a live goal thermometer (`[donor_merchant campaign="N"]` and `[donor_merchant_progress campaign="N"]`).
* New: Donor self-service portal - passwordless magic-link dashboard for giving history, receipts, and managing recurring gifts through Stripe's billing portal (`[donor_merchant_portal]`).
* New: Printable receipts (save as PDF) for every donation, plus year-end annual tax statements, served securely over signed links.
* New: Offline / manual donation entry from the admin (checks, cash, bank, other).
* New: Tax-receipt settings (Tax ID/EIN, organization address, tax-deductible language) and a donor-portal page setting.
* Improved: Donations CSV export now includes the campaign column.
* Developer: `donor_merchant_suppress_receipt` and `donor_merchant_suppress_admin_notification` filters.

= 2.0.0 =
* Complete rewrite for production.
* Stripe Payment Element with one-time and recurring (monthly/quarterly/yearly) donations.
* PayPal Smart Buttons for one-time donations.
* Fee recovery, tributes/memorials, and donor notes.
* Donor and donation management with search, filters, and CSV export.
* Customizable email receipts and admin notifications.
* Stripe webhooks with signature verification (renewals, refunds, failures).
* Reports dashboard.
* Donation Form block for the editor; legacy [donation_form] shortcode still supported.
* Internationalization-ready; 20 currencies.

= 1.0 =
* Initial prototype.
