Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-site-buy/src/assets.rs

//! **The buy pages' browser scripts, ONE copy for every product site** (owner 2026-10-23):
//! Gunnar's sign-up page script (`auth/signup.html`) and welcome page script
//! (`auth/welcome.html`), moved here as they are, and the order page a vendor sends a buyer
//! back to (`/paid/{reference}`, `/cancel/{reference}`: [`paid_js`], T16). A site supplies only its [`Settings`] (its
//! names, its product word, how a request carries its session) and draws its own look; its UI
//! server serves what [`buy_js`] and [`welcome_js`] render. No product server serves them.
//!
//! The scripts carry `@@NAME@@` words where a site differs; each is filled in inside a
//! single-quoted JavaScript string, escaped.

/// Gunnar's sign-up page script, with the sites' words to fill in.
pub const BUY_JS: &str = include_str!("../assets/buy.js");
/// Gunnar's welcome page script, with the sites' words to fill in.
pub const WELCOME_JS: &str = include_str!("../assets/welcome.js");

/// The order page's script (`/paid/{reference}`, `/cancel/{reference}`): Gunnar's FollowOrder,
/// with the sites' words to fill in (TASKS-4-v2 T16, A28 M11: Holger's hand port and Gunnar's
/// wasm panes were two copies that drifted).
pub const PAID_JS: &str = include_str!("../assets/paid.js");

/// **The elements a site's order page carries** for [`paid_js`] to fill: the cancelled box,
/// the steps, the headline and its sentence, the change and room lines, the progress line, the
/// pay area, the fault line and the foot. The site's page draws the look around them.
pub const PAID_IDS: [&str; 10] = ["cancelled", "steps", "headline", "detail", "change", "room", "progress", "pay", "fault", "foot"];

/// **Where the order page leads**, beside a site's [`Settings`].
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Back {
    /// "Try again" (an order that is gone, or a cancelled checkout): Holger's
    /// `/signup?plan=account`, Gunnar's `/account/storage`.
    pub again: String,
    /// "Back to your account": Holger's `/plan`, Gunnar's `/account`.
    pub account: String,
    /// The sign-in page a signed-out reader is sent to, `?next=` the order's address: `/login`.
    pub login: String,
}

/// **A site's settings block**: everything that differs between the sites' buy pages.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Settings {
    /// The product as the buyer reads it: "Gunnar", "Holger".
    pub product: String,
    /// The prefix of the page's browser-storage keys: `gunnar_`, `holger_`.
    pub storage: String,
    /// The free account's plan id: `gunnar/account`.
    pub free_account: String,
    /// The free row's note, in the owner's words for the product ("Free: clone only. Pushing
    /// needs a paid plan.").
    pub free_note: String,
    /// The free account's card-check line, the vendor's and the product's words ("A card check
    /// at Stripe (0 kr) proves you are a person."); empty: none shown (RA5-41).
    pub card_check: String,
    /// The beta line on every tab ("Beta: we replicate your data, but cannot guarantee it.");
    /// empty: none shown (RA5-41).
    pub beta_line: String,
    /// Where an instance is managed, its company appended: `/signup?plan=private&renew=`.
    pub manage: String,
    /// The `?plan=` word, beside Gunnar's `10gb`, that opens the account choice (`account`).
    pub account_plan: String,
    /// What the site is to the buyer, in "Nothing is sold by this …": `console`, `site`.
    pub here: String,
    /// Where this tab keeps the session as a bearer (`holger-bearer-token`); empty: the site's
    /// cookie carries it.
    pub bearer_key: String,
    /// The site's own "who is here": `/auth/status`, or Holger's `/-/signup`.
    pub status_url: String,
    /// ★ A32-24: how [`Self::status_url`] answers — empty: the fields `/auth/status` names;
    /// [`STATUS_LINKS`]: a sign-up links answer (`signed_in`, `account`, `account_url`, …),
    /// which the script reads as those fields. The script branches on this, never on a path.
    pub status_shape: String,
    /// The shop's word on what it sells here (`/auth/buy/offer`); empty: `status_url` says it.
    pub offer_url: String,
    /// Where a refused or unknown visitor is told to write; empty: the site's front page.
    pub contact: String,
}

/// [`Settings::status_shape`] of a site whose "who is here" answers sign-up links.
pub const STATUS_LINKS: &str = "links";

/// Pure: `v` inside a single-quoted JavaScript string.
fn quoted(v: &str) -> String {
    v.chars()
        .map(|c| match c {
            '\\' => "\\\\".to_owned(),
            '\'' => "\\'".to_owned(),
            '\n' => "\\n".to_owned(),
            '\r' => "\\r".to_owned(),
            '<' => "\\x3c".to_owned(),
            c => c.to_string(),
        })
        .collect()
}

fn fill(template: &str, s: &Settings) -> String {
    fill_with(template, s, &Back::default())
}

fn fill_with(template: &str, s: &Settings, b: &Back) -> String {
    let words = [
        ("AGAIN", &b.again),
        ("ACCOUNT", &b.account),
        ("LOGIN", &b.login),
        ("PRODUCT", &s.product),
        ("STORAGE", &s.storage),
        ("FREE_ACCOUNT", &s.free_account),
        ("FREE_NOTE", &s.free_note),
        ("CARD_CHECK", &s.card_check),
        ("BETA_LINE", &s.beta_line),
        ("MANAGE", &s.manage),
        ("ACCOUNT_PLAN", &s.account_plan),
        ("HERE", &s.here),
        ("BEARER_KEY", &s.bearer_key),
        ("STATUS_URL", &s.status_url),
        ("STATUS_SHAPE", &s.status_shape),
        ("OFFER_URL", &s.offer_url),
        ("CONTACT", &s.contact),
    ];
    words.iter().fold(template.to_owned(), |t, (k, v)| t.replace(&format!("@@{k}@@"), &quoted(v)))
}

/// **The sign-up page's script for a site.**
pub fn buy_js(s: &Settings) -> String {
    fill(BUY_JS, s)
}

/// **The welcome page's script for a site.**
pub fn welcome_js(s: &Settings) -> String {
    fill(WELCOME_JS, s)
}

/// **The order page's script for a site**: its words from `s` (the product, the bearer key)
/// and its ways on from `b`.
pub fn paid_js(s: &Settings, b: &Back) -> String {
    fill_with(PAID_JS, s, b)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn gunnar() -> Settings {
        Settings {
            product: "Gunnar".into(),
            storage: "gunnar_".into(),
            free_account: "gunnar/account".into(),
            free_note: "Free, with no commitment.".into(),
            manage: "/signup?plan=private&renew=".into(),
            account_plan: "10gb".into(),
            here: "console".into(),
            status_url: "/auth/status".into(),
            contact: "desk@mock.invalid".into(),
            ..Settings::default()
        }
    }

    /// ★ **Every word a site differs in is filled in, and nothing else is left to fill.**
    #[test]
    fn a_sites_scripts_carry_its_words_and_nothing_left_to_fill() {
        for js in [buy_js(&gunnar()), welcome_js(&gunnar())] {
            assert!(!js.contains("@@"), "a word left unfilled");
        }
        let buy = buy_js(&gunnar());
        assert!(buy.contains("out.push({ mode: 'private', words: TAB_PRIVATE });") && buy.contains("const TAB_PRIVATE = 'New private instance';"), "BUY-ONE-2's tab name");
        assert!(buy.contains("const CARD_CHECK_KEY = 'gunnar_card_check';") && buy.contains("plan: 'gunnar/account', next: '/' }"));
        assert!(buy.contains("const SITE = { bearer: '', status: '/auth/status', statusShape: '', offer: '' };"));
        // Gunnar's own lines, as its console's tests read them (the plans are asked with no
        // session; `10gb` opens the account).
        assert!(buy.contains("fetch('/auth/signup/ladder', { credentials: 'same-origin' })"));
        assert!(buy.contains("const asked = (q.get('plan') || '').toLowerCase();") && buy.contains("asked === '10gb' ? (page.status.authenticated ? 'grow-account' : 'account')"));
        let welcome = welcome_js(&gunnar());
        assert!(welcome.contains("const CONTACT = 'desk@mock.invalid';") && welcome.contains("'?subject=Gunnar%20signup'"));
    }

    /// ★ **RA5-41: the free row's note, the card-check line and the beta line are the site's.**
    /// The shared script hard-coded Gunnar's words ("Stripe (0 kr)", "Pushing needs a paid
    /// plan"), so a Holger site, or one on another vendor or currency, told its buyer them, and
    /// the site's `free_note` silently did nothing. Each is filled from the site's settings; an
    /// empty card-check or beta line shows none.
    #[test]
    fn the_free_note_card_check_and_beta_line_are_the_sites_words() {
        let holger = Settings {
            product: "Holger".into(),
            free_note: "Free: read and install only. Publishing needs a paid plan.".into(),
            card_check: "A card check at the vendor proves you are a person.".into(),
            beta_line: "Beta: Holger replicates your packages, but cannot guarantee them.".into(),
            ..gunnar()
        };
        let js = buy_js(&holger);
        for own in ["Free: read and install only. Publishing needs a paid plan.", "A card check at the vendor proves you are a person.", "Beta: Holger replicates your packages, but cannot guarantee them."] {
            assert!(js.contains(own), "the site's words: {own}");
        }
        for gunnars in ["Pushing needs a paid plan", "(0 kr)", "Beta: we replicate your data"] {
            assert!(!js.contains(gunnars), "Gunnar's words on another site: {gunnars}");
        }
        let quiet = buy_js(&Settings { card_check: String::new(), beta_line: String::new(), ..holger });
        assert!(quiet.contains("const BETA_LINE = '';"), "no beta line set: none shown");
    }

    /// ★ **The welcome page keeps no settle bound and no settle words of its own** (RA4-19): it
    /// shows the shop's sentences, and the one the shop sends ahead for its silence.
    #[test]
    fn the_welcome_page_has_no_settle_bound_or_words_of_its_own() {
        let js = welcome_js(&gunnar());
        for own in ["LONGER_MS", "2 * 60 * 1000", "this takes a few seconds", "your payment is safe", "Nothing has been charged, and this page keeps checking"] {
            assert!(!js.contains(own), "the page keeps its own settle rule or words: {own:?}");
        }
        assert!(js.contains("body.words_if_silent") && js.contains("body.silent_after_ms"), "the page shows the sentence the shop sends ahead");
    }

    /// ★ **The order page's script is ONE copy for every site** (T16, A28 M11): a site's words
    /// and its ways on are filled in, nothing is left to fill, and it follows the order over the
    /// shared relay's doors.
    #[test]
    fn the_order_pages_script_carries_a_sites_words_and_ways_on() {
        let back = Back { again: "/account/storage".into(), account: "/account".into(), login: "/login".into() };
        let js = paid_js(&gunnar(), &back);
        assert!(!js.contains("@@"), "a word left unfilled");
        assert!(js.contains("const BUY = { product: 'Gunnar', bearerKey: '', again: '/account/storage', account: '/account', login: '/login' };"), "{}", &js[..1200]);
        assert!(js.contains("'/api/account/subscription/status?reference='") && js.contains("'/api/account/subscription/cancel'"));
        for id in PAID_IDS {
            assert!(js.contains(&format!("$('{id}')")), "the script fills #{id}");
        }
        let holger = Settings { product: "Holger".into(), bearer_key: "holger-bearer-token".into(), ..gunnar() };
        let js = paid_js(&holger, &Back { again: "/signup?plan=account".into(), account: "/plan".into(), login: "/login".into() });
        assert!(js.contains("product: 'Holger', bearerKey: 'holger-bearer-token', again: '/signup?plan=account', account: '/plan'"));
    }

    /// ★ **A32-24: the script reads a sign-up links answer where the SITE says so** (its
    /// `status_shape`), whatever path it is served at: no product's path decides it.
    #[test]
    fn the_status_shape_is_the_sites_setting_not_a_path() {
        let links = Settings { status_url: "/who".into(), status_shape: STATUS_LINKS.into(), ..gunnar() };
        let js = buy_js(&links);
        assert!(js.contains("status: '/who', statusShape: 'links'"), "the site's word is filled in");
        assert!(js.contains("if (SITE.statusShape !== 'links') return l;"), "the script branches on the setting");
        assert!(!BUY_JS.contains("SITE.status !=="), "never on the status path itself");
    }

    /// ★ **A word is a word, never code**: a quote, a backslash or a `</script>` in a setting
    /// stays inside its string.
    #[test]
    fn a_setting_never_breaks_out_of_its_string() {
        let s = Settings { product: "O'Neil\\</script>".into(), ..gunnar() };
        let js = buy_js(&s);
        assert!(js.contains("'New private O\\'Neil\\\\\\x3c/script>'"), "escaped");
        assert!(!js.contains("</script>"));
    }
}