=== SignSealer ===
Contributors: mashdun
Tags: esignature, waiver, contract, signature, agreement
Requires at least: 6.4
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.6.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Send waivers and agreements from WordPress, and see who has signed. Signing happens on SignSealer; this site keeps ids and status, never evidence.

== Description ==

Your site sends somebody a document to sign, and later wants to know whether they signed it. That is the whole job of this plugin.

Everything that makes a signature worth having happens on SignSealer: consent recorded as its own event before the signature is accepted, the document's exact text fingerprinted into every entry on the trail, and a hash chain that breaks if any entry is altered. A certificate can be checked afterwards by anyone holding its code, without an account.

**You need a SignSealer account.** This plugin is a client for it. See https://signsealer.com.

= Three things this plugin refuses to do =

These are limits on purpose, not features that are missing.

**It never renders a signature pad.** Signing happens on SignSealer's own pages. A drawing captured in WordPress and posted to an API would make every claim on the certificate untrue — the certificate says what the signing engine observed, and it cannot observe something that arrived as a form field.

**It never stores evidence.** Document ids, a status and a link. If you restore a six-month-old backup of this site, nothing about any signature changes, because nothing about any signature was here.

**It never holds a master credential.** The connection is OAuth 2.0 with PKCE, scoped to one business, and you can end it from your own SignSealer account without touching WordPress. There is no key here that reaches two accounts.

= Why a link and not an embedded frame =

SignSealer's signing pages refuse to be framed (`frame-ancestors 'none'`). A signing page inside another site's frame is a clickjacking target, and the thing being clicked is a signature. So this plugin sends the signer to SignSealer's own page, where they can see whose address bar they are signing on.

= What it does =

* Connects your site to one SignSealer business over OAuth 2.0 with PKCE.
* Registers its own webhook endpoint when you connect, so there is no secret to copy between two screens.
* Verifies every delivery: the HMAC signature, a five-minute freshness window, and a replay it has already seen. Fires a `signsealer_event` action your own code can hook.
* Has its own **SignSealer** item in the admin menu. **Start here** says what the plugin does, whether the site is connected, and — for every integration — whether its plugin is on the site, whether it is set up, and the steps to set it up.
* Lists that business's forms and templates under **SignSealer → Templates**, with the shortcode for each one beside it.
* Gives you a **SignSealer agreement** block, and `[signsealer_sign workflow="your-form"]` if you prefer a shortcode.
* **WooCommerce:** mark a product as needing an agreement, and the document is started when the order is paid (or placed, your choice), offered on the thank-you page, in the order email and under My Account, with its status on the order screen and in the order notes. Or have it signed before payment: the customer signs on your site, in a frame, and the checkout will not place the order until it is signed.
* **WooCommerce Bookings:** a booking you confirm starts its agreement straight away, paid or not, and the booking's date, time, number of people and resource are filled into the template.
* **Gravity Forms, WPForms, Fluent Forms and Contact Form 7:** map a form to a workflow under **SignSealer → Forms** — which field is the email, which is the name, which blanks the template fills from the form — and a submission ends on the signing page, or with a link in the form's own confirmation.
* Sends the signer back to your site afterwards: the order they came from, or the page the form was on.

= For developers =

Every verified delivery fires an action:

`add_action( 'signsealer_event', function ( $event, $payload ) {
    if ( 'signing.completed' === $event ) {
        // $payload['document']['id'] has signed.
    }
}, 10, 2 );`

The signature is checked before this fires, so a handler can trust what it is handed.

== Installation ==

1. Install and activate the plugin.
2. Open **SignSealer → Start here** in the admin menu and press **Connect to SignSealer**.
3. Sign in to SignSealer (or create an account there), choose the business this site belongs to, and approve. You are brought straight back.

4. Back on Start here, pick the integrations you want. Each card has its three steps and a link to a step-by-step guide at https://signsealer.com/wordpress.

The plugin registers itself with SignSealer, registers its own webhook endpoint, and signed statuses start arriving with nothing to copy or paste. Your site must be on https: SignSealer checks the plugin is really running there before it connects, and https is also how it posts signed statuses back.

== Frequently Asked Questions ==

= Why does my site need to be on https? =

Two reasons. Before SignSealer connects a site it fetches a small page the plugin serves, to check the plugin is really running there and that the request came from this site; it will only do that over https, so nobody can register an application in your site's name. And once connected, SignSealer posts signed statuses back to your site, which must also be over https so those deliveries are private and verifiable.

= Do I need a SignSealer account? =

Yes. This plugin talks to the SignSealer API; it is not a standalone signing tool.

= Where is the signed document kept? =

On SignSealer, with its certificate and audit trail. This site keeps the document's id and its status so a page can say whether it is signed.

= Can I collect the signature on my own page? =

No, and that is deliberate. See "Three things this plugin refuses to do" above.

= What happens if I deactivate the plugin? =

Nothing at SignSealer. Your documents, evidence and certificates are unaffected. Deactivating does not revoke the connection — end that in SignSealer under Developers → Connected apps.

= Does it work with WooCommerce? =

Yes. On a product's edit screen, under General, choose the **SignSealer agreement** that product needs. When an order containing it is paid — or placed, under SignSealer → WooCommerce — the document is started for the customer, offered on the thank-you page, in their order email and under My Account, and its status appears on the order screen and in the order notes as SignSealer reports it. Works whether your orders live in posts or in WooCommerce's own order tables.

= Can the customer sign before they pay? =

Yes. Under SignSealer → WooCommerce choose **Before payment**. A customer whose cart holds a product that needs an agreement is sent to a page of your site that shows SignSealer's signing page in a frame; the classic checkout and the checkout block both refuse to place the order until SignSealer confirms it is signed, and the order then carries that signed document. SignSealer shows its signing page in a frame only on sites the business has proven, and the site you connected is one.

= Does it work with WooCommerce Bookings? =

Yes. A bookable product is a product, so mark it the same way. A booking you confirm starts its agreement at once, whether or not it has been paid, because the agreement is wanted before the day. The booking's date, time, number of people and resource go to the template as `booking_date`, `booking_time`, `persons` and `resource`, and the booking's own screen shows where the agreement stands.

= Does it work with Gravity Forms, WPForms, Fluent Forms or Contact Form 7? =

Yes. Under **SignSealer → Forms**, choose a workflow for a form and say which of its fields hold the email address and the name; add lines like `boat_name = 4` to fill a template blank from a field. After they submit, the person is sent straight to the signing page, or the form's own confirmation gains a link — your choice per form. The entry keeps the document's id, title and status, and a note on it says what was sent and what happened. Contact Form 7 keeps no entries, so there the link is carried to its own script and shown under the form's message.

= What about Ninja Forms, Formidable, Forminator, SureForms or MetForm? =

Not yet. Start here names any of them it finds on your site, with the way round it until then: give the form you want signed a public address in SignSealer, and set your form's confirmation to redirect to it (or to a page with the SignSealer agreement block). The person types their name and email again, and nothing is recorded on the entry.

= Where did Settings → SignSealer go? =

Since 1.5.0 SignSealer has its own item in the admin menu, with Start here, Forms, WooCommerce, Templates and Settings under it. Old bookmarks to the Settings pages still work: they take you to the same page in its new place.

= Does the signer come back to my site afterwards? =

Yes, when your site is on https. After signing, the page offers a way back to the order they came from, or the page the form was on. SignSealer only sends people to sites a business has proven it holds, and connecting this plugin proved this one.

== Screenshots ==

1. SignSealer → Settings once the site is connected. Connecting registers the
   webhook endpoint itself, so there is nothing to copy between two screens.
2. SignSealer → Templates: the forms and templates that business has, with the
   shortcode for each one beside it.
3. The plugin in the plugins list, which is where most people meet it.

== External services ==

This plugin connects to the SignSealer API at `https://api.signsealer.com` — the service it is a client for. It is required for the plugin to do anything.

**What is sent, and when:**

* When you press Connect: this site's address and a one-time token, so SignSealer can fetch `/wp-json/signsealer/v1/site` on this site, confirm the plugin is running here, and register an application for this site. Then the PKCE challenge, once, as the browser is sent to approve.
* On every call afterwards: an access token, and this site's address in the user-agent header so a support request can be traced.
* When you send a document: the template you chose, the values you filled in, and the signer's name and email address.
* When a WooCommerce order starts a document: the customer's name, email address and phone number if given, the order number, the product's name and quantity, and the address of the order-received page to send them back to.
* When a customer signs before payment: the name and email address they type, the product's name and quantity, and the address of the signing page on your site to send them back to. Their session keeps the document's id and signing link until the order is placed.
* When a form submission starts a document: the values of the fields you mapped (email address, name, phone, and any template blanks), and the address of the page the form was on.
* When somebody ticks a box to be texted, at checkout or on a form, before the document starts: their phone number, the words beside the box exactly as they saw them, and which form or order it was, on which site. SignSealer keeps this as the record of their consent to be texted about that document.
* Every ten minutes at most, while an administrator screen or the checkout asks: a request for whether your SignSealer business can send texts, and the wording to show beside a box to be texted.

**What is received:** template and workflow lists, document ids and statuses, signing links, and whether your business can send texts, with the wording for the box.

SignSealer is operated by Mashdun LLC.
Terms: https://signsealer.com/legal/terms
Privacy policy: https://signsealer.com/legal/privacy-policy
Subprocessors: https://signsealer.com/security/subprocessors

== Changelog ==

= 1.6.0 =
* The link can go by text as well as email. When your SignSealer plan
  includes texting, WooCommerce can ask at checkout whether the customer
  wants the link by text (SignSealer, WooCommerce), and each form under
  SignSealer, Forms can name a tick box as its texting consent field. A
  ticked box, and the phone number given, are recorded at SignSealer as the
  person's consent, in the words beside the box, before the agreement is
  sent, so the first text can go.
* Nothing to be texted is offered when the business cannot text: on a plan
  without texting, or on a test account. The screens say which.
* The words to put beside the box come from SignSealer: the opt-in wording
  your business declared, or the one the carriers approved on SignSealer's
  own forms, with your business's name.

= 1.5.1 =
* The thank-you page asks for the signature once, near the top. On a block
  theme (such as Twenty Twenty-Five) the "One more step" box came last, after
  the addresses, with a smaller list of the same documents above it; it now
  takes that list's place under the order.
* The Sign buttons on your site (the thank-you page, the block and the
  shortcode) are styled by block themes as well as classic ones. On a block
  theme they were plain links.
* The block's preview in the editor looks like the button your visitors
  will see, rather than a greyed-out one.
* The order's SignSealer box says Signed, Waiting for a signature, Declined
  and so on as a coloured status.
* Tidier cards on Start here: a status no longer wraps oddly under a long
  name, and a card's button and guide link no longer touch.

= 1.5.0 =
* SignSealer has its own item in the admin menu, with Start here, Forms,
  WooCommerce, Templates and Settings under it, instead of three entries
  under Settings. Old Settings addresses still work: they go to the same
  page in its new place, and a connection that was halfway through when you
  updated still finishes.
* **Start here**: what SignSealer does, whether this site is connected, and
  a card for every integration — WooCommerce, WooCommerce Bookings, Gravity
  Forms, WPForms, Fluent Forms, Contact Form 7 and the signing button — that
  says whether its plugin is on this site, whether it is set up, and the
  three steps to set it up, with a link to a step-by-step guide. Form
  plugins it does not work with yet are named, with the way round it.
* **SignSealer → Forms** is always in the menu, and explains itself; a site
  with none of the supported form plugins is told which ones work. The
  WooCommerce choice moved to its own page, which also lists the products
  that need an agreement.
* Connecting: coming back from SignSealer no longer shows "That connection
  attempt has expired" under "Connected" when the page loads twice, and a
  reload after connecting shows the connection rather than an error.
  Pressing Connect again after an attempt that did not finish reuses the
  application the first press registered instead of registering another.
  Turning down the connection on SignSealer's screen now says so. Somebody
  making their SignSealer account while connecting has thirty minutes
  rather than five.
* A link to Start here and Settings beside Deactivate on the plugins screen,
  and a one-line pointer there until the site is connected.

= 1.4.2 =
* Deleting the plugin now removes everything it stored. Until now it left
  the form mappings, the WooCommerce trigger setting, the links from each
  agreement back to its order or form entry, and a few short-lived caches
  in the database after the plugin itself was gone. Notes and agreement ids
  on your WooCommerce orders and form entries stay: they are part of those
  orders' and entries' history.
* That includes sites with a persistent object cache (Redis, Memcached),
  where cached copies are now removed along with the stored ones, and every
  site of a multisite network, not only the one it was deleted from.
* An agreement's status can no longer go backwards. SignSealer's notices
  about one agreement can arrive out of order (they are sent in parallel and
  retried), and a late "sent" arriving after "signed" set the agreement back
  to waiting: the WooCommerce order and the form entry asked for a signature
  that had already been given, and gained a note saying so. The newest
  status now wins, by when it happened.
* Checked by deleting the plugin from a real WordPress, with a persistent
  object cache on, from a single site and from a network, in the test
  suite, and by scanning every file for code PHP 7.4 cannot run.

= 1.4.1 =
* Gravity Forms: "Send them straight to the signing page" and "Show the
  form's own confirmation, with a link to sign" now happen. The plugin
  started the agreement from `gform_after_submission`, which Gravity Forms
  runs only after it has decided what the visitor sees, so the visitor got
  the form's usual confirmation and had to find the link in SignSealer's
  email. It now starts the agreement as soon as the entry is saved, before
  the confirmation is chosen.
* Gravity Forms: a submission Gravity Forms marks as spam no longer starts
  an agreement. Until now a bot's submission sent a real signing request to
  whatever address it typed.
* Checked against the real Gravity Forms, with forms submitted through its
  own submission process, rather than only against a stand-in for it.

= 1.4.0 =
* WooCommerce can ask for the signature before payment. With "Before
  payment" chosen under Settings -> SignSealer, a customer whose cart holds
  a product that needs an agreement signs it on a page of your site, in a
  frame, and the classic checkout and the checkout block will not place the
  order until SignSealer says it is signed. The order then carries that
  document instead of starting another.
* The connection asks for four scopes, not five: the plugin lists templates
  and never changes one, so it no longer asks to.
* SignSealer's own sentence is shown when it refuses something. Until now
  the plugin read the wrong part of the answer and showed only the status.

= 1.3.2 =
* Reconnecting a site no longer registers a second webhook endpoint. The
  check for the one already there read the wrong key in SignSealer's answer,
  so it never found it, and each reconnect added an endpoint that delivered
  every event again.

= 1.3.1 =
* The block's editor script now declares what it needs and carries the
  plugin's version. Without that, a browser kept the previous release's
  editor script until WordPress itself updated, and the block's own words
  in the editor could never be translated, because WordPress only sets up
  a script's translations when it depends on `wp-i18n`.
* Checked against WordPress 7.1.2 with the directory's Plugin Check (every
  category, low severity included: nothing found), the block loaded in the
  editor with no console errors, and the WooCommerce, WPForms, Fluent Forms
  and Contact Form 7 integrations driven end to end.

= 1.3.0 =
* **Connecting is one button.** The settings screen asked for an application
  id and secret, and nothing a site owner could reach issued them. Now the
  plugin registers itself: it mints a one-time token, serves it at
  `/wp-json/signsealer/v1/site`, and SignSealer fetches that page over https
  to confirm the plugin is running here before registering an application
  named after this site. Press Connect, sign in, approve, done. A partner
  with an application of their own can still paste it, under a fold.
* The screen says plainly when the site is not on https, since that is the
  one thing that stops the connection.

= 1.2.0 =
* **WooCommerce Bookings.** A booking the shop confirms starts its agreement
  at once, paid or not; a paid booking starts it under the "paid" trigger.
  The booking's date, time, number of people and resource go to the
  template, and the booking's own screen shows where the agreement stands.
* **Contact Form 7.** The fourth form plugin on Settings → SignSealer forms.
  Its fields are listed by tag name; a sent submission starts the workflow;
  the signing link rides on the form's own answer to a small script that
  sends the person on or adds the link under the message, with the address
  in the message itself when the form is submitted without its script.

= 1.1.0 =
* **WooCommerce.** A product can need an agreement; the order starts it when
  paid or when placed, offers it on the thank-you page, in the customer's
  order email and under My Account, and carries its status on the order
  screen and in the order notes. Compatible with WooCommerce's own order
  tables (HPOS).
* **Gravity Forms, WPForms and Fluent Forms.** One screen under Settings maps
  a form to a workflow: the email, name and phone fields, template blanks
  filled from fields, and whether the person is sent straight to the signing
  page or given a link in the confirmation. The entry keeps the document and
  its status, with a note for what was sent and what happened.
* **The way back.** A document started here carries the page to return the
  signer to — the order, or the page the form was on — which SignSealer
  honours because this site is the one the connection was made on. Needs
  https; on http the signed page simply has no button.
* A `signsealer_woocommerce_participant` filter and a
  `signsealer_form_participant` filter, for a site that keeps a template's
  blank somewhere the plugin cannot guess.

= 1.0.1 =
* **The settings screen told you to do something that cost money.** It said
  "In SignSealer under Developers → Webhooks, add this address" on a screen
  where connecting had already added it. Following that made a second endpoint
  for the same address, and two endpoints deliver every event twice and are
  counted twice. It now says it is registered, says not to add another, and
  keeps the manual form for the one case it is for.
* **The templates screen said you had no forms when it could not ask.** If the
  API did not answer, the screen showed the error and then both tables saying
  "None yet. Make one in SignSealer under Workflows" — so a business with forty
  forms was told it had none and invited to make them again. A failure now
  replaces the tables rather than captioning them.
* **A connection with no application behind it reported itself as connected.**
  Refreshing a token needs the application id and secret beside the refresh
  token; without them the plugin emitted two PHP notices, posted an empty
  `client_id`, and put the API's answer — "client_id is required" — at the top
  of an admin screen. Such a connection is now simply not a connection, and the
  screen offers connecting again.
* `wp_safe_redirect` with an `allowed_redirect_hosts` filter on the OAuth
  hand-off, fenced to the host `SIGNSEALER_API_BASE` resolves to.
* Tested up to WordPress 7.1. Translation template added.

= 1.0.0 =
* First release. OAuth connection, a self-registering and verified webhook receiver, the templates and forms screen, a block and a shortcode.

== Upgrade Notice ==

= 1.6.0 =
Customers can ask for the signing link by text as well as email, at checkout or on a form, when your SignSealer plan includes texting.

= 1.5.1 =
The Sign button is styled on block themes, and the thank-you page asks for the signature once, near the top.

= 1.5.0 =
SignSealer moves from Settings to its own menu item, with a Start here page that explains each integration. Old links still work.

= 1.4.2 =
Deleting the plugin now removes everything it stored.

= 1.4.1 =
Gravity Forms submissions now go straight to the signing page, and spam entries start nothing.

= 1.4.0 =
WooCommerce can require the agreement to be signed before payment.

= 1.3.2 =
Reconnecting no longer adds a duplicate webhook endpoint.

= 1.3.1 =
The block's editor script updates with the plugin and can be translated.

= 1.3.0 =
Connecting no longer asks for an application id and secret. Already
connected sites are unaffected.

= 1.2.0 =
WooCommerce Bookings and Contact Form 7. Nothing changes for a site that uses
neither.

= 1.1.0 =
WooCommerce, Gravity Forms, WPForms and Fluent Forms integrations, and the
signer is sent back to your site afterwards. Nothing changes for a site that
uses none of them.

= 1.0.1 =
Three screens that told you something untrue. One of them, if you did what it
said, registered a second webhook endpoint and doubled what you were billed
for deliveries.

= 1.0.0 =
First release.
