=== Donor Merchant - Donation Form with Recurring Donations, Stripe and PayPal ===
Contributors: mattmcwilliam
Tags: donation, donate, fundraising, recurring donations, nonprofit
Requires at least: 6.2
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 2.5.5
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Free donation plugin for nonprofits, churches and charities: one-time and recurring donations with Stripe and PayPal, donor portal and receipts.

== Description ==

**Donor Merchant is a free donation plugin for WordPress.** It gives a nonprofit, church, charity, school, or community group a donation form that takes one-time and recurring donations through Stripe and PayPal, keeps a record of every donor and gift in your own database, and sends receipts.

Every feature is in the free plugin. There are no paid add-ons, no locked settings, and no fee added on top of what Stripe or PayPal charge. The project is paid for by optional support plans: priority email help for a single organization, or faster help for agencies looking after several client sites. Those plans buy support, not features. Details are at [donormerchant.com/pricing](https://www.donormerchant.com/pricing/).

= Who it is for =

* **Small nonprofits and charities** that need a dependable donation page without a monthly platform bill.
* **Churches** taking online tithes and offerings, with funds such as General, Building, or Missions, and year-end giving statements donors can download themselves.
* **Schools, PTAs, booster clubs, animal rescues, and community groups** running a goal-based campaign.
* **Freelancers and agencies** who build sites for nonprofit clients and want one donation setup they can repeat on every site, with no license keys.

= Donations =

* **One-time and recurring donations.** Donors choose one-time, monthly, quarterly, or yearly. Recurring giving works on both gateways: Stripe subscriptions and PayPal subscriptions.
* **Stripe** through Stripe's Payment Element: cards, plus Apple Pay and Google Pay where you have them turned on in your Stripe account.
* **Bank debit (ACH)** from US bank accounts through Stripe, as a setting you switch on.
* **PayPal** Smart Buttons for one-time gifts.
* **20 currencies**, and a Number format setting that controls the decimal and thousands separators on the form, receipts, statements, and reports, or follows your site language.
* **Suggested amounts and a minimum.** Your own suggested amounts, a custom amount field, and a minimum donation the server enforces.
* **Fee recovery.** Donors can choose to add the estimated processing fee to their gift, worked out from the percentage and fixed fee you enter. A donor who pays a one-time gift by bank account is asked for the lower bank fee instead.
* **Designations.** Donors pick the fund their gift supports.
* **Tributes.** A gift can be made in honor of or in memory of someone, with an optional message.
* **Custom fields.** Add your own questions as text, paragraph, dropdown, or checkbox fields, each required or optional.
* **Test mode.** Test end to end with Stripe test keys and the PayPal sandbox before going live.

= Campaigns and goal thermometers =

Create as many campaigns as you need, each with its own goal, description, and suggested amounts. A campaign's form shows progress toward the goal, and the `[donor_merchant_progress campaign="N"]` shortcode places the same thermometer anywhere else on your site.

= Donor records and reports =

* Every donor and gift is saved in your WordPress database, with each donor's lifetime total and last donation date.
* A dashboard shows total raised, this month's total, number of donations and donors, recurring gifts, and the average gift.
* Search and filter the donations list by status, and export donations or donors to CSV.
* Record offline gifts (check, cash, bank transfer, or other) so your totals stay complete.
* Add a donor by hand, and correct a gift's status to completed, refunded, or failed without deleting it. Corrections never email the donor.
* An optional monthly fundraising summary is emailed to you on the first of each month.

= Receipts, statements, and the donor portal =

* Thank-you emails you can edit, with placeholders such as `{donor_name}`, `{amount}`, and `{receipt_link}`, plus a new-donation alert for you.
* A printable receipt for every gift, showing your organization's address, tax ID, and your own receipt wording. Donors save it as a PDF from their browser, so no PDF library is bundled.
* Annual giving statements for the current and previous year.
* A donor portal with passwordless sign-in by one-time email link, where donors see their giving history, open receipts and statements, and update the card on or cancel a Stripe recurring gift through Stripe's hosted billing portal.

= Donation importer =

The built-in donation importer reads a CSV export and recognizes the exports from GiveWP, Charitable, Donorbox, PayPal, Stripe, WooCommerce, Gravity Forms, and WPForms. Any other spreadsheet works too.

You confirm which column is which before anything is written, and a preview shows the result first. Re-running is safe: gifts already on file are matched by transaction ID, or by donor, amount, and date. Imported gifts never send receipts or fire integrations, and rows marked as test mode can be skipped. Each file can hold up to 10,000 rows.

One honest limit: active recurring donations cannot be moved by any plugin. They live at Stripe or PayPal, tied to the integration that created them, so donors set up a new recurring gift on your new form. Their past giving comes across in the import.

= Integrations =

* **Mailchimp.** Add donors to an audience, with a "keep me updated" checkbox on the form and optional double opt-in.
* **Outgoing webhooks.** Send completed donations to Zapier, Make, a CRM, or your own code. Each request is signed with HMAC-SHA256 so the receiver can confirm it came from your site.

= Design and placement =

* A Donation Form block, plus the shortcodes `[donor_merchant]`, `[donor_merchant campaign="N"]`, `[donor_merchant_progress campaign="N"]`, and `[donor_merchant_portal]`. A Shortcodes tab in Settings lists each one with your campaign IDs filled in, ready to copy.
* Six accent colors or a custom color, and three button shapes (pill, rounded, sharp), with a live preview in Settings.

= Records that match your processor =

Renewals, refunds, and bank payments that settle days later are recorded from Stripe's and PayPal's signed webhooks, so the donations list lines up with your gateway account. A partial refund leaves the gift in your totals, and only a full refund marks it refunded.

= How it stays secure =

Payment details are collected by Stripe and PayPal directly in the donor's browser. No card number, expiration date, or CVC is ever transmitted to or stored on your server. Donation totals are recomputed on the server, payment status is confirmed server-to-gateway, and webhook signatures are verified.

Card testing and spam are handled without a CAPTCHA by default: a hidden honeypot field, a signed check that rejects forms submitted impossibly fast, per-IP rate limiting, and a server-side minimum amount. If your payment processor asks you to add a CAPTCHA, the `donor_merchant_pre_donation` filter lets you plug one in without editing the plugin.

= What it does not do =

Donor Merchant is deliberately lean. It has no peer-to-peer fundraising, no text-to-give, no matching-gift lookup, no Gift Aid for UK charities, and no drag-and-drop form builder. Forms are set up from the Settings screens.

= External services =

To process donations this plugin connects to third-party payment and (optionally) marketing services. Nothing is sent until you enable and configure a given service.

* **Stripe** (only when Stripe is enabled). The Stripe.js library is loaded from `https://js.stripe.com` on pages that display the donation form, so payment details can be collected securely in the donor's browser. Your server also calls the Stripe API at `https://api.stripe.com` to create payment intents, customers, and subscriptions, and to verify payments. Data sent: donation amount and currency, the donor's name, email and phone number if given, an internal reference for the donation, and the payment details the donor enters (handled by Stripe - they do not touch your server). Stripe terms: https://stripe.com/legal - privacy: https://stripe.com/privacy
* **PayPal** (only when PayPal is enabled). The PayPal JS SDK is loaded from `https://www.paypal.com` on pages with the donation form. Your server calls the PayPal API (`https://api-m.paypal.com`, or `https://api-m.sandbox.paypal.com` in sandbox mode) to create and capture orders and subscriptions. Data sent: donation amount, currency, and an internal reference. PayPal terms: https://www.paypal.com/legalhub - privacy: https://www.paypal.com/privacy
* **Mailchimp** (optional - only when you enable it). By default the form shows a "keep me updated" checkbox and only opted-in donors are sent. If you switch that checkbox off in the settings, every donor is added to the audience, so leave it on unless you have another basis for contacting them. Your server calls the Mailchimp API (`https://<dc>.api.mailchimp.com`) to add the donor to the audience you choose. Data sent: donor email and name. Mailchimp terms: https://mailchimp.com/legal/terms/ - privacy: https://mailchimp.com/legal/privacy/
* **Outgoing webhooks** (optional). If you enter one or more webhook URLs, completed-donation data is POSTed to those URLs - which you control and choose (e.g. your Zapier or Make endpoint). No third party receives data unless you configure it.

= Getting started =

1. Install and activate the plugin.
2. Go to **Donor Merchant → Settings → Payments** and paste your Stripe and/or PayPal API keys.
3. Add the **Donation Form** block (or `[donor_merchant]`) to any page.
4. If you accept recurring gifts, add the webhook URL shown on the Payments tab to your Stripe or PayPal account.
5. Test with Stripe test mode or the PayPal sandbox, then switch to live.

== Frequently Asked Questions ==

= Is Donor Merchant really free? =

Yes. Every feature described on this page is in the free plugin, with no add-ons to buy and no fee added to donations. You pay only Stripe's or PayPal's own processing fees. The optional paid plans are support plans: they get you priority help, not extra features.

= Can I accept recurring donations for free? =

Yes. Monthly, quarterly, and yearly donations work with both Stripe and PayPal in the free plugin. Set up the gateway's webhook (the Payments tab shows the exact URL and events) so each renewal payment is recorded.

= Does it work for churches? =

Yes. Churches use it for online tithes and offerings. Set up funds such as General, Building, or Missions so givers can designate their gift, let members give monthly, record cash and check offerings by hand so each giver's history is complete, and let members download their own year-end giving statement from the donor portal.

= Can a charity outside the US use it? =

Yes. It supports 20 currencies, including EUR, GBP, CAD, AUD, and NZD, and the Number format setting lets amounts follow your local conventions. Bank debit (ACH) is for US bank accounts only. UK charities should know there is no Gift Aid feature.

= Can I import donations from GiveWP or Charitable? =

Yes. Export your donations to CSV from GiveWP or Charitable, then upload the file at **Donor Merchant → Import**. The donation importer detects the columns automatically, including Charitable's campaign, date, and gateway columns, so gifts land in the right campaign. It also recognizes exports from Donorbox, PayPal, Stripe, WooCommerce, Gravity Forms, and WPForms. You review the mapping and a preview before anything is saved, and running it twice does not create duplicates.

= Will my donors' recurring donations move over when I switch plugins? =

No, and no plugin can do this. An active recurring donation lives at Stripe or PayPal, attached to the plugin that created it. Your import brings across each donor's past gifts. Ask recurring donors to start a new recurring gift on your new form, then cancel the old one.

= Is it PCI compliant? =

Card details are typed into fields hosted by Stripe's Payment Element or into PayPal's own window. They never reach your server, and the plugin never stores them. Integrations built this way generally qualify for the shortest PCI self-assessment questionnaire (SAQ A), as Stripe's security guide describes for its Payment Element. Your organization is still responsible for completing whatever PCI validation your processor asks for, and your site must use HTTPS.

= Does the plugin store card numbers? =

No, never. Card details are entered into fields hosted by Stripe (or on PayPal's site) and never reach your server.

= Do I need an SSL certificate? =

Yes. Stripe and PayPal require your site to be served over HTTPS.

= Where do I find my Stripe API keys? =

In your Stripe dashboard under **Developers → API keys**. Use the test keys with Test mode to try things out safely.

= Can donors cover the processing fees? =

Yes. Turn on fee recovery and enter your fee percentage and fixed fee. Donors then see a checkbox offering to add the estimated fee to their gift so the full amount reaches you. If you accept bank (ACH) payments, a donor who switches a one-time gift to their bank account is asked for the bank fee instead (by default 0.8%, capped at $5.00, Stripe's standard US price).

= Can donors choose which fund their gift goes to? =

Yes. Add your funds on the Forms tab and a designation dropdown appears on the form. The fund is saved with the gift, carried on recurring renewals, and shown in your donations list and CSV export.

= Can I record cash and check donations? =

Yes. Use **Add Donation** on the Donations screen to record a check, cash, bank transfer, or other offline gift. It counts toward the donor's lifetime total, your reports, and any campaign goal.

= Does it send tax receipts? =

Every completed gift gets an email receipt and a printable receipt page. Add your organization's address, tax ID, and the wording your accountant recommends, such as a statement that no goods or services were provided. Donors can also download an annual giving statement. The plugin does not give tax advice, so check what your country requires.

= Are webhooks required? =

Recommended, but not required for one-time card gifts. Recurring renewal payments, refunds, and bank payments that settle later are recorded from the Stripe and PayPal webhooks, so set them up if you accept recurring gifts or ACH. The settings page shows the exact URL and events to use.

= Does it include a CAPTCHA? =

No CAPTCHA is bundled. Card-testing attacks are limited by a hidden honeypot field, a timing check, per-IP rate limiting, and a server-side minimum donation, while Stripe and PayPal run their own fraud screening on every payment. If your processor asks for a CAPTCHA, a developer can add one through the `donor_merchant_pre_donation` filter without changing the plugin.

= Can I connect it to Mailchimp, Zapier, or my CRM? =

Yes. Mailchimp is built in. For Zapier, Make, a CRM, or custom code, add an outgoing webhook URL and every completed donation is posted to it as signed JSON.

= Does the plugin run background tasks or phone home? =

No. Donor Merchant sends no telemetry and performs no analytics, tracking, or license checks. It never contacts our servers.

When a page loads and nobody is donating, the plugin makes no external or background server requests. The donation form and the goal thermometer render entirely from your own database, and there is no AJAX polling or heartbeat activity.

On pages that contain the donation form, the visitor's browser loads Stripe.js and/or the PayPal SDK from the gateway's own CDN. That is required so card details go straight into the gateway's secure fields and never touch your server. No donor or payment data is sent until someone submits, and on pages without a form those scripts are not loaded at all.

Your server contacts Stripe or PayPal only when a visitor submits a donation, when a gateway sends an incoming signature-verified webhook, when a donor manages a recurring gift in the donor portal, and, if you enable them, when an outgoing webhook or Mailchimp sync fires on a completed donation.

The plugin schedules two WordPress cron events, and neither makes an external request. The first runs daily and, on the first of the month, emails the fundraising summary to your own admin address if that option is enabled. The second also runs daily and deletes donation attempts that were never completed and have sat unfinished or failed for more than 90 days; completed and refunded records, and anything attached to a recurring gift, are never touched. Filter `donor_merchant_stale_donation_days` to change the window, or set it to 0 to switch the cleanup off.

= Does it work with my theme? =

The form ships with clean, self-contained styling that adapts to any well-built theme, and everything is namespaced to avoid conflicts. Pick an accent color and button shape on the Forms tab to match your site.

= Does it do peer-to-peer fundraising or Gift Aid? =

No. Peer-to-peer fundraising pages, text-to-give, matching gifts, and Gift Aid are not part of Donor Merchant. It focuses on a donation form, accurate records, and receipts.

== Screenshots ==

1. The donation form: giving frequency, suggested amounts, an optional dedication, and fee recovery so the full gift reaches the cause.
2. Dashboard with fundraising totals at a glance and the most recent gifts.
3. Donations list with status filters, search, and one-click CSV export.
4. Donor list with lifetime giving totals and last donation date.
5. Payment settings: Stripe and PayPal as separate panels, each showing whether it is connected.
6. Form style settings: six accent colors with a live preview, plus button shape.
7. A campaign form with its own goal thermometer, description, and suggested amounts.

== Changelog ==

= 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.

== Upgrade Notice ==

= 2.5.5 =
Fixes PayPal recurring refunds and renewal amounts, Charitable split-campaign imports, day-first import dates and donor-portal sign-in behind page caches. Bank (ACH) donors covering fees now pay the bank fee, not the card fee.

= 2.5.4 =
Fixes Stripe renewals losing their fund and, on newer Stripe accounts, their refunds; fixes PayPal recurring gifts recorded twice when the donor never returned from PayPal. Adds a Number format setting and a smoother Charitable import.

= 2.5.3 =
Fixes two rate limiting faults that could turn away genuine donors, and repairs the Add donation screen. Adds manual donor entry, a Shortcodes tab, and automatic cleanup of abandoned attempts.

= 2.5.2 =
Small appearance fixes to the donation form: form controls in the same row now line up exactly, and the optional dedication section is tidier.

= 2.5.1 =
Security release. Hardens the public donation endpoint, PayPal subscription activation, and Stripe webhook handling. Recommended for all users.

= 2.3.1 =
Security hardening: CSV export formula-injection protection and stricter PayPal subscription verification. Recommended for all users.

= 2.3.0 =
Adds an automatic monthly fundraising summary email and documents the developer hooks. No database changes.

= 2.2.0 =
Adds ACH bank payments, recurring PayPal, designations/funds, Zapier & Mailchimp integrations, and custom form fields. Database updates automatically; existing data is preserved.

= 2.1.0 =
Adds campaigns with goal thermometers, a donor self-service portal, printable/annual tax receipts, and offline donation entry. Database updates automatically; existing donations and settings are preserved.

= 2.0.0 =
Complete production rewrite. Settings carry over; the [donation_form] shortcode keeps working. Raw card fields are gone - payments now run through Stripe's hosted Payment Element and PayPal.
