diff --git a/frontend/404.html b/frontend/404.html index 795bd43e..ff4a631b 100644 --- a/frontend/404.html +++ b/frontend/404.html @@ -115,6 +115,6 @@

Page not found

- + diff --git a/frontend/about.html b/frontend/about.html index f06cf71b..d9bdaff8 100644 --- a/frontend/about.html +++ b/frontend/about.html @@ -291,6 +291,6 @@

Found a security issue?

- + diff --git a/frontend/account.html b/frontend/account.html index edf5c7c0..465594e6 100644 --- a/frontend/account.html +++ b/frontend/account.html @@ -399,7 +399,7 @@

Deactivate account.

- + diff --git a/frontend/apply-nav.py b/frontend/apply-nav.py index 915de7cd..53723522 100644 --- a/frontend/apply-nav.py +++ b/frontend/apply-nav.py @@ -30,8 +30,10 @@ # the desktop bar, and js/nav-auth.js rewrites its whole inside after the # session check. The two secondary routes a phone loses from the bar -- Sign in # and Help -- therefore hang under it as their own strip, which nav.js opens -# and closes with the drawer and nav-auth.js removes once you are signed in -# (the user menu carries both from then on). +# and closes with the drawer. Signed in, nav-auth.js rewrites that strip down +# to Help alone: Sign in is not an action you still need, and Help is, on the +# one surface a phone has left for it. It used to REMOVE the strip there, which +# is how a signed-in phone ended up with no route to support at all. NEW_MOBILE = '''\ - + diff --git a/frontend/ct-log.html b/frontend/ct-log.html index 8cb466b2..4855177c 100644 --- a/frontend/ct-log.html +++ b/frontend/ct-log.html @@ -429,6 +429,6 @@

Verify a hash

- + diff --git a/frontend/dashboard.html b/frontend/dashboard.html index 0a9702a5..3a4c0b3e 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -546,7 +546,15 @@

Questions people ask before they trust this

What does it cost?
-
Community is free forever, with no card. Paid plans start at €15 a month excl. btw, which is ParaSend Pro. See the plans
+ +
Community is free forever, with no card. Sending starts at €15 a month excl. btw, which is ParaSend Pro; signing starts at €49 a month excl. btw, which is ParaSign Pro. See the plans
Who can read what I send?
@@ -640,7 +648,7 @@

Finish setting up your account

- + diff --git a/frontend/developer.html b/frontend/developer.html index 9f06c2a7..cd6f6320 100644 --- a/frontend/developer.html +++ b/frontend/developer.html @@ -156,7 +156,7 @@

API key created

- + diff --git a/frontend/docs.html b/frontend/docs.html index 6ddcc4f9..55a1eb46 100644 --- a/frontend/docs.html +++ b/frontend/docs.html @@ -1041,7 +1041,7 @@

Frequently asked questions

- + - + diff --git a/frontend/help/authenticator-apps.html b/frontend/help/authenticator-apps.html index 5c1ad414..9c36b89b 100644 --- a/frontend/help/authenticator-apps.html +++ b/frontend/help/authenticator-apps.html @@ -306,6 +306,6 @@

Security overview

- + diff --git a/frontend/help/authenticator-setup.html b/frontend/help/authenticator-setup.html index 41438b9b..f7b896c8 100644 --- a/frontend/help/authenticator-setup.html +++ b/frontend/help/authenticator-setup.html @@ -407,6 +407,6 @@

API key or TOTP?

- + diff --git a/frontend/help/backup-codes.html b/frontend/help/backup-codes.html index 6b40a3a3..a2d41122 100644 --- a/frontend/help/backup-codes.html +++ b/frontend/help/backup-codes.html @@ -379,6 +379,6 @@

Set up your authenticator

- + diff --git a/frontend/help/gmail-extension.html b/frontend/help/gmail-extension.html index 3f154eb5..3e4413f6 100644 --- a/frontend/help/gmail-extension.html +++ b/frontend/help/gmail-extension.html @@ -413,6 +413,6 @@

API key vs TOTP

- + diff --git a/frontend/help/index.html b/frontend/help/index.html index 7cd8eca4..43d024d9 100644 --- a/frontend/help/index.html +++ b/frontend/help/index.html @@ -363,6 +363,6 @@

Troubleshooting

- + diff --git a/frontend/help/iot-integration.html b/frontend/help/iot-integration.html index 59e2b576..abf18337 100644 --- a/frontend/help/iot-integration.html +++ b/frontend/help/iot-integration.html @@ -394,6 +394,6 @@

Full API reference

- + diff --git a/frontend/help/lost-authenticator.html b/frontend/help/lost-authenticator.html index c53bf8c2..e20291de 100644 --- a/frontend/help/lost-authenticator.html +++ b/frontend/help/lost-authenticator.html @@ -365,6 +365,6 @@

Set up your authenticator

- + diff --git a/frontend/help/outlook-extension.html b/frontend/help/outlook-extension.html index b1bbe0c4..061b7055 100644 --- a/frontend/help/outlook-extension.html +++ b/frontend/help/outlook-extension.html @@ -401,6 +401,6 @@

API key vs TOTP

- + diff --git a/frontend/help/session-issues.html b/frontend/help/session-issues.html index 56e79ed4..676a3602 100644 --- a/frontend/help/session-issues.html +++ b/frontend/help/session-issues.html @@ -368,6 +368,6 @@

Backup codes

- + diff --git a/frontend/index.html b/frontend/index.html index 1e2bb75d..b7e12891 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -57,6 +57,34 @@ .hp-btn:focus-visible{outline:3px solid rgba(29,78,216,.35);outline-offset:2px} .hp-btn:active{transform:translateY(1px)} +/* ---------- signed in: the hero is the whole page ---------- + js/home-auth.js stamps data-session="in" on once the session check + comes back authenticated. Everything under the hero is the sales page, and a + customer who has already bought does not need to be sold to: on a phone the + three actions were followed by eight screens of pitch, ending in the founder + letter. The selector names the sections rather than a list of ids, so a + section added tomorrow is covered the day it lands. The nav, the hero and + the footer stay; the footer is where the legal documents are. Signed out, + nothing here applies and the page is exactly what it was. */ +html[data-session="in"] main > section:not(.home-hero){display:none} +/* The art goes with the pitch it illustrates. It is a document that gets + signed, sealed and then burns, on a 16-second loop, and it exists to explain + the product to someone who does not have it yet. It is also the reason the + hero cannot simply be made shorter: .hp-doc is absolutely positioned and + draws about 120px BELOW the 380px box .hp-art reserves for it, which is + invisible while the sales page continues underneath and becomes a document + cut in half the moment the hero is the last thing in
(main has + overflow:hidden). Measured at 1440 signed in: hero 472px, .hp-doc bottom 80px + past it, receipt and signature sliced through. A phone has hidden this art + since it shipped, for its own reason; signed in, every width does. */ +html[data-session="in"] .hp-art{display:none} +/* With the art gone the second grid column is empty, so the hero is one column + at every width. The wrap keeps its own 1120px width, which is what lines the + hero up with the nav and the footer; the heading, the lede and the founder + line carry their own ch-based measures, so nothing stretches. */ +html[data-session="in"] .home-hero .hp-wrap{grid-template-columns:minmax(0,1fr)} +html[data-session="in"] .home-hero{padding-bottom:56px} + /* ---------- hero ---------- */ .home-hero{position:relative;padding:28px 0 32px;background:linear-gradient(180deg,#F8FAFC 0,var(--hp-paper) 160px)} .home-hero .hp-wrap{position:relative;display:grid;gap:28px} @@ -588,7 +616,7 @@

Rules you can check. Every one has a proof.

- - + + diff --git a/frontend/js/error-message.js b/frontend/js/error-message.js new file mode 100644 index 00000000..f2d1a816 --- /dev/null +++ b/frontend/js/error-message.js @@ -0,0 +1,68 @@ +'use strict'; +// What a customer is told when something we did not plan for goes wrong. +// +// The signer had one sentence for a wrong authenticator code, one for an +// account without an authenticator, and for everything else it rethrew the +// error it caught. That error's message is whatever the wire said: "http_500", +// "internal_error", "Failed to fetch", or the string a browser puts on a +// TypeError. Someone stuck halfway through signing a contract read that and +// had no idea whether to wait, to retry, or to call someone. Two of those +// three strings do not even name a problem. +// +// So there is one sentence for the unplanned case, it says what to do next, +// and it names an address that reaches a human. The technical detail is not +// thrown away: it goes to the console, which is where it was useful anyway. +// +// Loadable three ways on purpose, following js/parasign-pdf-ops.js: node +// requires it (the suite that tests this file), a page loads it as a plain +// script and reads self.paramantErrors (js/parashare.page.js is a classic +// script and cannot import), and an ES module side-effect-imports it and reads +// the same namespace. One copy of the sentence, so the two products cannot +// drift into two different apologies. + +(function (root, factory) { + if (typeof module === 'object' && module.exports) module.exports = factory(); + else root.paramantErrors = factory(); +})(typeof self !== 'undefined' ? self : this, function () { + + var SUPPORT_FAILURE_MESSAGE = + 'Something went wrong on our side. Try again in a minute; if it keeps happening, ' + + 'mail privacy@paramant.app with the time and what you did.'; + + // The cases we DID plan for. Each tells the customer something he can act on + // that the generic sentence cannot, so they keep their own words. Every other + // code, and every error with no code at all, is unplanned by definition. + var KNOWN = { + totp_required: 'Enter the 6-digit code from your authenticator app.', + totp_invalid: 'That authenticator code didn’t match. Try the current 6-digit code.', + totp_unavailable: 'Set up an authenticator app on your account first, then sign with its code.', + }; + + function isKnownFailure(error) { + var code = error && error.code; + return typeof code === 'string' && Object.prototype.hasOwnProperty.call(KNOWN, code); + } + + // The translator. Errors in, one readable sentence out, never a raw message. + // A thrown TypeError, a 500 from the relay and an error object from a browser + // extension all land on the same sentence, because to the person reading it + // they are the same event: this did not work and it was not his fault. + function userFacingMessage(error) { + return isKnownFailure(error) ? KNOWN[error.code] : SUPPORT_FAILURE_MESSAGE; + } + + // Log the technical detail, return the error to throw in its place. `where` + // is a short label naming the step, so a console holding three failures still + // says which one broke. The replacement carries code 'service_error' unless + // the original had a code of its own, and callers that render a message + // switch on that code rather than on the sentence. + function reportFailure(where, error) { + try { console.error('[paramant] ' + where, error); } catch (ignored) { /* a missing console is never why a flow dies */ } + var replacement = new Error(userFacingMessage(error)); + replacement.code = isKnownFailure(error) ? error.code : 'service_error'; + replacement.cause = error; + return replacement; + } + + return { SUPPORT_FAILURE_MESSAGE: SUPPORT_FAILURE_MESSAGE, KNOWN: KNOWN, isKnownFailure: isKnownFailure, userFacingMessage: userFacingMessage, reportFailure: reportFailure }; +}); diff --git a/frontend/js/home-auth.js b/frontend/js/home-auth.js index ac5fd014..283cdeee 100644 --- a/frontend/js/home-auth.js +++ b/frontend/js/home-auth.js @@ -4,6 +4,15 @@ * and is what link scrapers see). When a valid session is present, swap to the * logged-in variant: "Welcome back" + quick actions, marketing badges gone. * + * Swapping the hero was only half of it. Everything BELOW the hero is the sales + * page -- why half of it is free, the two products, what it costs, who is + * behind it -- and it stayed on screen for a customer who had already bought. + * A signed-in phone got three actions and then eight screens of pitch. So this + * also stamps data-session="in" on , and index.html hides every section + * after the hero off that one attribute. The hiding is CSS, not JS: no inline + * script (CSP script-src 'self'), and nothing here walks the section list, so + * a new section is covered the day it is added. + * * Uses the same probe the nav uses (GET /api/user/session/verify, credentials * included). CSP-safe: external file, no inline script, no eval. Best-effort, * tolerant: any failure leaves the logged-out pitch in place. @@ -30,6 +39,7 @@ out.hidden = true; inn.hidden = false; + document.documentElement.setAttribute('data-session', 'in'); }) .catch(function () { /* stay on the logged-out pitch */ }); })(); diff --git a/frontend/js/nav-auth.js b/frontend/js/nav-auth.js index cee4b9cf..33814d8f 100644 --- a/frontend/js/nav-auth.js +++ b/frontend/js/nav-auth.js @@ -53,16 +53,28 @@ function renderLoggedIn(email) { setNavigation(APP_NAV, 'Workspace'); - // The drawer tail offers Sign in and Help to a visitor who is not signed - // in. Once you are, both are wrong there: the user menu below carries - // Help, and Sign in is not an action you still need. + // Support survives signing in. The tail used to be REMOVED here, on the + // reasoning that the user menu carries Help from then on. On a phone that + // left no Help at all: the bar sheds .nav-help below 700px, the drawer is + // pinned to the five workspace links, and the menu behind the email + // address is not where anyone looks for support. A signed-in customer with + // a stuck signature had to type the url. + // + // So the strip stays and carries the one route the bar handed over. Sign in + // does go: it is not an action you still need. Help lands in the same place + // as it does signed out, one tap under the drawer, 48px tall. var tail = document.getElementById('nav-mobile-tail'); - if (tail) tail.remove(); + if (tail) tail.innerHTML = 'Help'; var shortEmail = email.length > 24 ? email.slice(0, 18) + '...' : email; + // Help sits where it sits signed out: a text link left of the account + // control. nav.css hides it below 700px, where the drawer tail above takes + // over, and gives it a 44px target from 1023px down. + // // Never interpolate the email into innerHTML (stored/self DOM XSS): the // signup regex permits HTML metacharacters. Build static markup, then set // the email via textContent (mirrors home-auth.js / dashboard.js). container.innerHTML = + 'Help' + ' - + diff --git a/frontend/press.html b/frontend/press.html index 73f48a6b..e6dd2b92 100644 --- a/frontend/press.html +++ b/frontend/press.html @@ -341,6 +341,6 @@

Press contact

- + diff --git a/frontend/pricing.html b/frontend/pricing.html index 12bd0503..91b72060 100644 --- a/frontend/pricing.html +++ b/frontend/pricing.html @@ -604,7 +604,7 @@

- + diff --git a/frontend/privacy.html b/frontend/privacy.html index 8f8ba660..94a7706b 100644 --- a/frontend/privacy.html +++ b/frontend/privacy.html @@ -340,6 +340,6 @@

Contact

- + diff --git a/frontend/request-key.html b/frontend/request-key.html index 75833f69..b055e052 100644 --- a/frontend/request-key.html +++ b/frontend/request-key.html @@ -171,6 +171,6 @@

- + \ No newline at end of file diff --git a/frontend/rules.html b/frontend/rules.html index 5b52660e..9848bf83 100644 --- a/frontend/rules.html +++ b/frontend/rules.html @@ -299,6 +299,6 @@

Honest errors over convenient lies

- + diff --git a/frontend/security.html b/frontend/security.html index 7f299dcc..c2160f51 100644 --- a/frontend/security.html +++ b/frontend/security.html @@ -515,6 +515,6 @@

Customer transparency

- + diff --git a/frontend/security/acknowledgements.html b/frontend/security/acknowledgements.html index 0fe2cc73..93c003f3 100644 --- a/frontend/security/acknowledgements.html +++ b/frontend/security/acknowledgements.html @@ -195,6 +195,6 @@

How to be listed

- + diff --git a/frontend/sign-flow.js b/frontend/sign-flow.js index 7cfe67b8..5a94032a 100644 --- a/frontend/sign-flow.js +++ b/frontend/sign-flow.js @@ -2816,6 +2816,10 @@ async function doSign() { else if (e && e.code === 'cancelled') msg = 'Signing cancelled. Tap Sign now when you’re ready.'; else if (e && (e.code === 'totp_invalid' || e.code === 'totp_required')) msg = 'That authenticator code didn’t match. Tap Sign now and enter the current 6-digit code.'; else if (e && e.code === 'totp_unavailable') msg = 'Set up an authenticator app on your account first (Account → Two-factor), then sign with its code.'; + // Already translated by the signer (js/error-message.js): the message on + // this error is our own vetted sentence with a next step, never the wire's + // "http_502" or a browser's TypeError text. The detail is in the console. + else if (e && e.code === 'service_error') msg = e.message; else if (e && (e.code === 'prf_unsupported' || e.code === 'need_passkey')) msg = 'Your passkey can’t do one-tap signing here. Tap Sign now to sign with your authenticator code instead.'; else if (e && (e.status === 403 || e.status === 409 || e.status === 410)) msg = 'That signing authorization was already used or has expired. Tap Sign now to start a fresh one.'; else if (e && e.status) msg = 'Signing could not be completed right now (server error ' + e.status + '). Please try again in a moment.'; diff --git a/frontend/sign.html b/frontend/sign.html index 57e2fff9..e68e6593 100644 --- a/frontend/sign.html +++ b/frontend/sign.html @@ -891,7 +891,7 @@

How to use your signature

- + diff --git a/frontend/signup.html b/frontend/signup.html index 8e24b7d6..973ab880 100644 --- a/frontend/signup.html +++ b/frontend/signup.html @@ -289,7 +289,7 @@

Check your inbox.

- + diff --git a/frontend/signup/verified.html b/frontend/signup/verified.html index d7b663f5..ddfa4997 100644 --- a/frontend/signup/verified.html +++ b/frontend/signup/verified.html @@ -11,7 +11,7 @@ - +