=== WP Push Notify ===
Contributors: yourname
Tags: push notifications, web push, notifications
Requires at least: 5.8
Requires PHP: 7.4
Tested up to: 6.6
Stable tag: 1.9.3
License: GPLv2 or later

Self-hosted browser push notifications. All subscriber data and sending happens on your own server.
No external libraries or Composer step required — install and activate is the entire setup.

Website, documentation, and troubleshooting: https://wppushnotify.com

== Setup (before you sell this) ==

1. Create a product at dashboard.freemius.com, get your plugin ID + public key,
   and drop the Freemius PHP SDK into a `vendor/freemius` folder here (their "Getting
   Started" page gives you a download with your keys pre-filled).
2. Paste the id/slug/public_key into wp-push-notify.php where marked.
3. Set your pricing/plans in the Freemius dashboard (this is where the
   monthly subscription itself lives — you never touch billing code).
4. Zip the plugin and upload it as your product's package in Freemius;
   they'll host downloads/updates for you.

== What ships where ==

* Subscriber table (wp_wppn_subscribers) — lives in the buyer's own database. Never transmitted anywhere.
* VAPID keypair — generated per-install on activation using PHP's built-in openssl extension, stored in wp_options on the buyer's site.
* Notification sending — buyer's server calls FCM/Mozilla push endpoints directly, using a from-scratch RFC 8291/8292 implementation (no external library), validated against the official IETF test vectors.
* License check — Freemius SDK, roughly daily, confirms the subscription is active. That's the only outbound call tied to you.

== 1.9.3 ==
Website links and a clearer path from Free to Premium. No changes to
sending, subscribers, or settings.

* The plugin's website address (https://wppushnotify.com) is now the product page.
* Free version: a small "You're using the free version" panel on the Push
  Notifications page explains the 200-subscribers-per-send limit and links to the
  Premium plans. One license covers one website.
* The About section now links to the documentation, troubleshooting guide,
  plugin updates, and support contact.

== 1.8.0 ==
Hosting-aware, adaptive sending architecture. No unrelated features —
this release only changes how sends are paced against the server's real
capacity; the Web Push system and everything else is unchanged.

* Hosting environment now has 5 tiers instead of 3: Shared, Business,
  Cloud, VPS, and Dedicated (up from Shared/Business/VPS). Batch size,
  delay between batches, and per-push-service concurrency all scale up
  across the tiers, e.g. Shared stays at 50/batch, 5s delay; the new
  Cloud tier sits at 400/batch, 2s delay; the new top Dedicated tier
  reaches 1,000/batch, 1s delay, 100 concurrent requests per push
  service.
* Auto-detect now also does a best-effort CPU core-count check (where
  shell_exec()/nproc are available — commonly not on shared hosting,
  which is fine since it's only ever a bonus signal there) and surfaces
  it on the settings page alongside the memory_limit/max_execution_time
  it already showed. Deliberately never auto-selects Dedicated: a large
  cloud instance or VPS reports the same kind of PHP-level settings as
  an actual dedicated server, so there's no reliable way to tell them
  apart automatically — auto-detect's ceiling is VPS, and Dedicated is
  a manual choice for a buyer who knows that's genuinely their setup.
* New: live back-pressure during sending. On top of the static
  per-tier numbers, every batch now also checks the server's ACTUAL
  memory usage (against its own memory_limit) and, where readable,
  system load average (against detected/assumed CPU count) right
  before running. If either looks like the server is under real
  strain, that batch is automatically shrunk and slowed down below the
  tier's normal numbers instead of proceeding at full speed — this is
  the "back off instead of pushing through" safeguard, and it self-
  corrects on the next batch once things settle, no configuration
  needed.
* The 1,000,000-subscriber safety ceiling, the free-tier 200-subscriber
  cap, the concurrency-per-push-service cap, the 20-second per-batch
  time budget, and the stalled-send self-healing are all unchanged —
  this release only adds tiers and the live back-pressure check on top
  of that existing foundation.

== 1.7.0 ==
WooCommerce integration, smarter bell-click popup positioning, and
hosting auto-detection.

WooCommerce (all opt-in, off by default — new "WooCommerce" section under
Push Notifications, only shown when WooCommerce is active):
* New order placed: broadcasts a "social proof" notification to all
  subscribers once an order's payment is confirmed (Processing/Completed),
  e.g. "Someone just ordered {product}." No customer name, email, or other
  personal detail is ever included; fires at most once per order.
* Back in stock: broadcasts to all subscribers on a genuine Out of Stock
  -> In Stock transition.
* Price drop: broadcasts to all subscribers when a product's price drops
  by at least a configurable percentage (default 5%), so routine 1-cent
  price syncs don't trigger a notification.
* Abandoned cart reminder: unlike the three above, this is sent to only
  the ONE visitor who left the cart, not a broadcast. Uses a first-party
  cart-token cookie (works for logged-out shoppers) to correlate a
  subscribed browser with its WooCommerce session cart; a new 15-minute
  cron checks for carts inactive past a configurable delay (default 60
  minutes) and sends a single targeted push directly, bypassing the
  broadcast queue entirely. Cart contents are always read server-side
  from WooCommerce's own session, never trusted from the client. New
  wp_wppn_cart_subscribers table (self-creates on upgrade, cleaned up on
  uninstall the same as the other tables).
* All four have editable title/message templates with placeholders
  ({product}, {total}, {old_price}, {new_price}, {items}).

Subscription prompt:
* Clicking the bell now opens the popup anchored to the OPPOSITE side of
  wherever the bell is positioned (e.g. a bottom-left bell opens the
  popup bottom-right) instead of always using the fixed configured
  position — fixes the popup landing cramped in the same corner as the
  bell (or overlapping it) and being hard to read. Only applies to the
  bell-click path; the timed auto-popup and the [wppn_subscribe_button]
  shortcode still use the configured Popup Position setting, since
  neither has a bell to anchor against.
* Corner/edge-positioned popups now use a smaller margin and more of the
  available width on narrow (under 600px) screens, instead of staying
  pinned to their desktop size and crowding the edge.

Hosting / performance:
* New "Auto-detect (recommended)" hosting-environment option, now the
  default. Reads the server's own PHP memory_limit and max_execution_time
  on every send and picks Shared/Business/VPS automatically — since this
  plugin runs on each buyer's own hosting, this means it adapts to
  whatever that actually is (and re-adapts automatically if they move to
  better hosting later) with no configuration needed. The settings page
  shows exactly what was detected and why. Manual selection of a specific
  tier is still available for anyone who wants to override it.

== 1.6.0 ==
Design and UX improvements, ported over from a design review:

* The floating bell now visually reflects subscription state: blue
  (not yet subscribed), green with a small checkmark badge (already
  subscribed), grey (blocked in the browser) — previously every state
  showed the same blue bell. It also now updates live the moment someone
  subscribes via the popup modal, instead of only refreshing on next
  page load.
* Added toast feedback during and after the subscribe flow: a
  "Turning on notifications…" message while the service worker/REST call
  is in progress, and a specific, honest message if permission wasn't
  granted — including the common case where a Chromium browser silently
  blocks the permission prompt entirely inside Incognito/private windows,
  rather than assuming the visitor clicked "Block." All three messages
  are editable under Settings.
* The popup, thank-you card, and bell are now fully keyboard-accessible:
  proper focus trapping (Tab cycles within the dialog, Escape closes it)
  and an inert-attribute background lock instead of no lock at all.
* The auto-popup now waits for a detected cookie-consent banner
  (Complianz, CookieYes, Cookie Notice, GDPR Cookie Consent, OneTrust,
  cookieconsent.js) to clear before showing, instead of potentially
  colliding with it on screen.
* Fixed the bell's tooltip running off the left edge of the viewport when
  the bell is positioned on the left side of the screen, and added a
  narrow-mobile-viewport wrap safety net for long tooltip text.
* Clicking the shortcode subscribe button when notifications are already
  blocked now shows a clear message instead of silently doing nothing.

== 1.5.0 ==
* uninstall.php now deletes all plugin data via a "wppn_" option-name
  prefix match instead of an explicit list of option names — more
  maintainable, and can't silently miss a newly-added option the way an
  explicit list already had twice in this plugin's history.
* The pushsubscriptionchange handler now prefers the exact original
  applicationServerKey from event.oldSubscription.options when the
  browser provides it (guaranteed correct), falling back to the
  registration URL's config only when that isn't available.
* Added TROUBLESHOOTING.md with the most common real-world causes of
  "nothing shows up" / "works inconsistently" reports (stale per-device
  state, incognito testing pitfalls, cache-by-device-type mismatches,
  the .js.php WAF quarantine issue, WP-Cron reliability, and CORS setup).

== 1.4.3 ==
* The "Later"/dismissal state for the subscribe popup is now backed up in
  a cookie alongside localStorage (whichever has the higher dismissal
  count wins and re-syncs the other). On browser configurations that
  restrict or partition localStorage more aggressively than cookies
  (e.g. Firefox Enhanced Tracking Protection Strict), the popup no longer
  re-appears on every single visit for those visitors. This is a
  functional/preference cookie only — no personal data, not used for
  tracking — worth a one-line mention in your privacy policy the same way
  you'd disclose any "remembers your choice" cookie. Actual subscription
  status was already unaffected by this, since it's tracked via
  Notification.permission and the browser's own Push API state, both of
  which already survive a normal cookie/localStorage clear.

== 1.4.2 ==
* Added a manual "Plan Mode" setting under Advanced (Free/Premium),
  visible only while the real Freemius SDK isn't wired up yet. Lets you
  test the actual free-tier experience (200-subscriber cap) or unrestricted
  premium behavior on demand from one single plugin build, without needing
  real Freemius credentials configured. Automatically stops being consulted
  the moment a real license is connected — no code changes needed then.

== 1.4.1 ==
* Removed the fixed 30,000-subscriber-per-send ceiling for premium users.
  Premium is now uncapped (only a very high, filterable safety ceiling
  remains via the wppn_max_subscribers_per_send filter), matching how
  other genuinely self-hosted push plugins price/limit this — there's no
  per-subscriber delivery cost on this plugin's end to justify a fixed
  cap the way there is for plugins that route delivery through a paid
  cloud service. The free-tier cap (200 subscribers/send) is unchanged.

== 1.4.0 ==
High-concurrency sending for large subscriber lists (30,000+), plus a new
hosting-tuned performance setting.

* Sends to more than one batch's worth of subscribers now go out
  concurrently via curl_multi (grouped and capped per destination push
  service — FCM, Mozilla, WNS, Apple — rather than one uncapped pool)
  instead of one HTTP request at a time. Automatically falls back to the
  previous sequential behavior on any server where curl_multi isn't
  available, so nothing breaks on more locked-down hosts.
* New "Server Performance & Delivery Speed" setting under Advanced: choose
  Shared / Business / VPS to control batch size, per-service concurrency,
  and delay between batches. Defaults to the safest (Shared) setting.
* The 20-second per-batch time budget and the 404/410 dead-subscription
  cleanup are unchanged in behavior, now applied consistently across both
  the concurrent and sequential code paths.
* The concurrent sending path gets the same endpoint-host allowlist check
  as before, plus its own direct public-IP resolution check, since it
  doesn't automatically inherit WordPress core's SSRF protection the way
  the sequential wp_safe_remote_post() path does.

== 1.3.0 ==
Security hardening and reliability fixes from a full pre-launch audit.

Security:
* Fixed a server-side request forgery (SSRF) risk: the /subscribe REST
  route now validates that an "endpoint" is actually a recognized
  browser push-service host (FCM, Mozilla, WNS, Apple) before it's ever
  stored, and the sender re-validates again before making the outbound
  HTTP request — which now also uses wp_safe_remote_post() instead of
  wp_remote_post() for WordPress core's own private-IP/loopback
  protection as a second layer. Applies to CSV import too.
* Added a basic per-IP rate limit to the public /subscribe endpoint to
  curb scripted abuse/flooding.
* Renamed the service worker loader file away from a "*.js.php" double
  extension — that pattern is a known malware-disguise signature some
  hosts' security scanners/WAFs quarantine or block on sight.
* Fixed CSV/formula injection in the subscriber CSV export.
* Uninstalling the plugin can now actually delete all of its data
  (subscribers, send history, VAPID keys, settings) — opt-in via a new
  checkbox under Advanced, off by default so a routine
  deactivate/reactivate never loses data.

Bug fixes:
* Fixed a real bug in Advanced Settings: saving that tab ran the service
  worker path through sanitize_title(), which silently turns "wppn-sw.js"
  into "wppn-sw-js.js" (WordPress core converts "." to "-" there) — even
  when the field wasn't touched. Now uses sanitize_file_name() instead.
* Fixed a batch-sending bug: deleting expired subscriptions mid-send
  shifted the old OFFSET-based pagination, silently skipping subscribers
  on any send spanning more than one batch. Batches now resume from a
  stable subscriber-ID cursor instead, which isn't affected by deletions.
* Large sends now self-heal if WP-Cron doesn't run one of the scheduled
  batches (common on hosts that disable WP-Cron, or low-traffic sites) —
  a stalled send is automatically resumed on the next wp-admin visit,
  plus a new admin notice if WP-Cron is detected as disabled.
* "Notify subscribers on publish" now uses transition_post_status
  instead of publish_post, so it can apply to custom post types (opt in
  via the new wppn_autosend_post_types filter) instead of silently doing
  nothing for anything but the built-in Post type.
* Password-protected posts are now skipped by autosend, instead of
  pushing the title/excerpt to every subscriber regardless of the
  password gate.
* The "Allowed external domains" CORS check now normalizes scheme/host
  casing and trailing slashes before comparing, instead of requiring an
  exact string match that silently failed on trivial formatting
  differences.

Reliability / compatibility:
* The service worker now calls skipWaiting()/clients.claim() on
  install/activate, so a future update to it takes effect for
  already-open tabs instead of waiting for every tab to close.
* Added a pushsubscriptionchange handler so a subscription the browser
  rotates/invalidates on its own gets automatically renewed instead of
  silently going dead with no recovery path.
* The [wppn_subscribe_button] shortcode button now uses event
  delegation instead of a one-time binding, so it works even when
  injected after page load (AJAX content, page-builder popups, lazy-
  loaded sections).
* Sites without a Site Icon set (Settings > General) now fall back to
  this plugin's own bundled icon for the manifest, the iOS home-screen
  icon, and the notification icon, instead of shipping with no icon at
  all and quietly breaking iOS installability.
* Admin-configurable popup/thank-you text is now rendered via
  textContent instead of innerHTML on the front end — defense in depth,
  since the values were already HTML-stripped server-side.

== 1.2.6 ==
Fixes a caching-plugin issue that could make subscribing fail intermittently
(no subscriber saved, no thank-you popup shown):

* The plugin now sends explicit "never cache" signals (headers +
  DONOTCACHEPAGE + WP Rocket / W3 Total Cache reject-URI filters) on its
  service worker, manifest, and subscribe/unsubscribe routes, so a
  caching plugin can no longer serve a stale/broken copy of them.
* Self-healing: if WordPress's rewrite rules ever lose the service
  worker route (can happen if another plugin flushes rewrites at the
  wrong time), the plugin detects and re-flushes it automatically on the
  next wp-admin page load, and purges the most common caching plugins
  (WP Rocket, LiteSpeed, W3TC, WP Super Cache, WP Fastest Cache,
  SiteGround Optimizer, Autoptimize).
* subscribe.js now excludes itself from common JS defer/delay/combine
  optimizations (WP Rocket, LiteSpeed, Autoptimize) that could otherwise
  run it out of order.
* subscribe.js now checks the actual result of the service worker
  registration and the /subscribe REST call instead of assuming success
  — failures are logged to the browser console with the likely cause
  instead of silently doing nothing.

Buyer note: if a site's cache was already broken before updating, one
manual "clear cache" in their caching plugin clears out any bad copies
left over from before this fix was installed.
