diff --git a/app/shop_routes.py b/app/shop_routes.py index dae76c0..57d8f18 100644 --- a/app/shop_routes.py +++ b/app/shop_routes.py @@ -111,6 +111,7 @@ async def shop_browse(q: str = Query(""), genre: str = Query(""), style: str = Q items = [dict(r) for r in (await db.execute(text(f""" SELECT i.release_id, coalesce(min(i.title), dr.title) AS title, dr.artists_sort AS artist, dr.year, dr.country, min(i.price)::float AS price, count(*) AS copies, + (SELECT thumb FROM disc_cache dc WHERE dc.release_id=i.release_id) AS thumb, (SELECT artist_id FROM disc_release_artist ra WHERE ra.release_id=i.release_id ORDER BY ra.position LIMIT 1) AS artist_id, (SELECT string_agg(DISTINCT genre_name, ', ') FROM disc_release_genre g WHERE g.release_id=i.release_id) AS genre FROM inventory i JOIN disc_release dr ON dr.id = i.release_id @@ -142,6 +143,7 @@ async def shop_release(release_id: int, db=Depends(get_db)): """Public release detail — metadata + tracklist + in-stock copies (with the Woo buy link).""" dr = (await db.execute(text(""" SELECT dr.id, dr.title, dr.artists_sort AS artist, dr.country, dr.year, dr.notes, dr.master_id, + (SELECT thumb FROM disc_cache dc WHERE dc.release_id=dr.id) AS thumb, (SELECT artist_id FROM disc_release_artist ra WHERE ra.release_id=dr.id ORDER BY ra.position LIMIT 1) AS artist_id, (SELECT string_agg(label_name || coalesce(' ('||catno||')',''), ', ') FROM disc_release_label WHERE release_id=dr.id) AS label, (SELECT string_agg(name || coalesce(' '||descriptions,''), ', ') FROM disc_release_format WHERE release_id=dr.id) AS format, diff --git a/wp-bridge/README.md b/wp-bridge/README.md new file mode 100644 index 0000000..a6e234b --- /dev/null +++ b/wp-bridge/README.md @@ -0,0 +1,26 @@ +# RecordGod Bridge (WordPress plugin) + +Thin successor to WowPlatter. **RecordGod owns the catalog, stock, pricing and shipping**; this plugin is a +storefront skin + checkout adapter over its `/shop` API. WordPress/WooCommerce is kept lean — Woo only ever +sees the records people actually buy. + +## What it does + +| Concern | How | +|---|---| +| **Storefront (SEO)** | `class-rg-storefront.php` — server-renders `/records` (browse) and `/release/{id}` inside the theme, with ``, meta description, and JSON-LD `MusicAlbum`/`Offer`. Pulls `/shop/browse` + `/shop/release`. | +| **On-the-fly Woo product** | `class-rg-cart.php` — the WowPlatter trick. `/?rg_add=<sku>` looks the SKU up (`wc_get_product_id_by_sku`); if absent, mints a hidden `WC_Product_Simple` from `/shop/item` (price, image, stock=1) then adds it to the cart. 25k catalog, ~handful of Woo products. | +| **Shipping** | `class-rg-shipping.php` — a `WC_Shipping_Method` that quotes postage from `/shop/shipping/quote?units=<cart item count>` (AusPost Parcel/Express flat rate, 280g/record). Add it to a Woo shipping zone. | +| **Order webhook** | `class-rg-orders.php` — on `order_status_completed`, POSTs the order back to `/shop/woo-order` (with `X-Bridge-Key`). RecordGod marks the SKUs sold + logs the online sale. Idempotent. | +| **Settings** | Settings → RecordGod Bridge: base URL, bridge key (must match RecordGod's `bridge_key` secret), store id. | + +## Install + +1. Copy `wp-bridge/` to `wp-content/plugins/recordgod-bridge/` and activate (flushes rewrite rules). +2. Settings → RecordGod Bridge: set base URL (`https://recordgod.com`) + bridge key. +3. WooCommerce → Settings → Shipping → add **RecordGod Postage** to your AU zone. +4. Visit `/records`. (If pages 404, re-save Permalinks once.) + +## Not in scope (stays where it is) +- Woo handles payment + transactional emails. +- RecordGod sends its own receipts (`mailer.py`) and holds Discogs/import auth — no OAuth in this plugin. diff --git a/wp-bridge/includes/class-rg-api.php b/wp-bridge/includes/class-rg-api.php new file mode 100644 index 0000000..a2828c1 --- /dev/null +++ b/wp-bridge/includes/class-rg-api.php @@ -0,0 +1,39 @@ +<?php +if (!defined('ABSPATH')) exit; + +/** Thin HTTP client for the RecordGod /shop API — GET (transient-cached) + POST (bridge-key). */ +class RG_API { + + static function base() { + return rtrim(get_option('rg_base_url', 'https://recordgod.com'), '/'); + } + + /** GET /shop/<path>?args — decoded JSON or null. Cached in a transient for $ttl seconds (0 = no cache). */ + static function get($path, $args = [], $ttl = 120) { + $url = self::base() . $path . ($args ? '?' . http_build_query($args) : ''); + $key = 'rg_' . md5($url); + if ($ttl) { + $hit = get_transient($key); + if ($hit !== false) return $hit; + } + $r = wp_remote_get($url, ['timeout' => 12, 'headers' => ['Accept' => 'application/json']]); + if (is_wp_error($r) || wp_remote_retrieve_response_code($r) !== 200) return null; + $data = json_decode(wp_remote_retrieve_body($r), true); + if ($ttl && $data !== null) set_transient($key, $data, $ttl); + return $data; + } + + /** POST JSON to /shop/<path> with the bridge key header. Returns [code, decoded-body]. */ + static function post($path, $body) { + $r = wp_remote_post(self::base() . $path, [ + 'timeout' => 15, + 'headers' => [ + 'Content-Type' => 'application/json', + 'X-Bridge-Key' => get_option('rg_bridge_key', ''), + ], + 'body' => wp_json_encode($body), + ]); + if (is_wp_error($r)) return [0, ['error' => $r->get_error_message()]]; + return [wp_remote_retrieve_response_code($r), json_decode(wp_remote_retrieve_body($r), true)]; + } +} diff --git a/wp-bridge/includes/class-rg-cart.php b/wp-bridge/includes/class-rg-cart.php new file mode 100644 index 0000000..5ffc9a1 --- /dev/null +++ b/wp-bridge/includes/class-rg-cart.php @@ -0,0 +1,63 @@ +<?php +if (!defined('ABSPATH')) exit; + +/** + * On-the-fly Woo products — the WowPlatter trick. RecordGod holds the 25k catalog; Woo only ever sees the + * ~handful of items someone actually buys. A Woo product is minted the moment a SKU is added to cart, looked + * up by SKU thereafter (never duplicated), and kept out of the Woo shop loop (catalog_visibility=hidden). + */ +class RG_Cart { + + static function init() { + add_action('template_redirect', [__CLASS__, 'handle_add']); + } + + /** Ensure a Woo product exists for $sku (creating it from /shop/item if absent). Returns product id or 0. */ + static function ensure_product($sku) { + $id = wc_get_product_id_by_sku($sku); + if ($id) return $id; + + $item = RG_API::get('/shop/item/' . rawurlencode($sku), [], 0); + if (!$item || empty($item['in_stock'])) return 0; + + $p = new WC_Product_Simple(); + $p->set_name(trim(($item['artist'] ? $item['artist'] . ' — ' : '') . $item['title'])); + $p->set_sku($sku); + $p->set_regular_price((string) $item['price']); + $p->set_price((string) $item['price']); + $p->set_catalog_visibility('hidden'); // never floods the Woo shop loop + $p->set_manage_stock(true); + $p->set_stock_quantity(1); // single physical copy + $p->set_sold_individually(true); + $p->set_weight('0.28'); // 230g record + 50g packaging (Woo fallback; real quote is RG_Shipping) + if (!empty($item['release_id'])) $p->update_meta_data('_rg_release_id', $item['release_id']); + $id = $p->save(); + + if ($id && !empty($item['thumb'])) self::attach_thumb($id, $item['thumb']); + return $id; + } + + /** GET ?rg_add=<sku> → mint product, add to cart, bounce to the cart page. */ + static function handle_add() { + if (empty($_GET['rg_add'])) return; + $sku = sanitize_text_field(wp_unslash($_GET['rg_add'])); + $id = self::ensure_product($sku); + if ($id) { + WC()->cart->add_to_cart($id, 1); + wp_safe_redirect(wc_get_cart_url()); + } else { + wc_add_notice('Sorry, that record has just sold.', 'error'); + wp_safe_redirect(home_url('/records')); + } + exit; + } + + /** Sideload the Discogs thumb as the product image (once). */ + static function attach_thumb($product_id, $url) { + require_once ABSPATH . 'wp-admin/includes/media.php'; + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/image.php'; + $att = media_sideload_image($url, $product_id, null, 'id'); + if (!is_wp_error($att)) set_post_thumbnail($product_id, $att); + } +} diff --git a/wp-bridge/includes/class-rg-orders.php b/wp-bridge/includes/class-rg-orders.php new file mode 100644 index 0000000..700beda --- /dev/null +++ b/wp-bridge/includes/class-rg-orders.php @@ -0,0 +1,45 @@ +<?php +if (!defined('ABSPATH')) exit; + +/** On order completion, post it back to RecordGod so it marks the SKUs sold + logs the online sale. */ +class RG_Orders { + + static function init() { + add_action('woocommerce_order_status_completed', [__CLASS__, 'push'], 10, 1); + } + + static function push($order_id) { + $order = wc_get_order($order_id); + if (!$order || $order->get_meta('_rg_synced')) return; // idempotent (RecordGod also dedups on WC-<id>) + + $items = []; + foreach ($order->get_items() as $line) { + $product = $line->get_product(); + $sku = $product ? $product->get_sku() : ''; + if (!$sku) continue; + $items[] = [ + 'sku' => $sku, + 'name' => $line->get_name(), + 'qty' => (int) $line->get_quantity(), + 'price' => (float) $order->get_item_total($line, false), + ]; + } + if (!$items) return; + + [$code, $body] = RG_API::post('/shop/woo-order', [ + 'order_number' => (string) $order->get_order_number(), + 'email' => $order->get_billing_email(), + 'name' => trim($order->get_formatted_billing_full_name()), + 'total' => (float) $order->get_total(), + 'items' => $items, + ]); + + if ($code === 200) { + $order->update_meta_data('_rg_synced', current_time('mysql')); + } else { + $order->update_meta_data('_rg_sync_error', "HTTP $code"); + $order->add_order_note('RecordGod sync failed (HTTP ' . $code . ') — retry from order actions.'); + } + $order->save(); + } +} diff --git a/wp-bridge/includes/class-rg-shipping.php b/wp-bridge/includes/class-rg-shipping.php new file mode 100644 index 0000000..1ac9ceb --- /dev/null +++ b/wp-bridge/includes/class-rg-shipping.php @@ -0,0 +1,48 @@ +<?php +if (!defined('ABSPATH')) exit; + +/** Woo shipping method that quotes postage from RecordGod (record-count → AusPost flat rate). */ +class RG_Shipping extends WC_Shipping_Method { + + function __construct($instance_id = 0) { + $this->id = 'recordgod'; + $this->instance_id = absint($instance_id); + $this->method_title = 'RecordGod Postage'; + $this->method_description = 'Live AusPost flat-rate quote from RecordGod (Parcel / Express Post).'; + $this->supports = ['shipping-zones', 'instance-settings', 'settings']; + $this->init(); + } + + function init() { + $this->init_form_fields(); + $this->init_settings(); + $this->enabled = $this->get_option('enabled', 'yes'); + $this->title = $this->get_option('title', 'Postage'); + add_action('woocommerce_update_options_shipping_' . $this->id, [$this, 'process_admin_options']); + } + + function init_form_fields() { + $this->instance_form_fields = [ + 'enabled' => ['title' => 'Enable', 'type' => 'checkbox', 'default' => 'yes'], + 'title' => ['title' => 'Title', 'type' => 'text', 'default' => 'Postage'], + ]; + } + + function calculate_shipping($package = []) { + $units = 0; + foreach ($package['contents'] as $line) $units += (int) $line['quantity']; + if ($units < 1) return; + + $q = RG_API::get('/shop/shipping/quote', ['units' => $units], 30); + if (!$q || empty($q['options'])) return; + + foreach ($q['options'] as $opt) { + $this->add_rate([ + 'id' => $this->id . ':' . $opt['service'], + 'label' => $opt['label'], + 'cost' => (float) $opt['price'], + 'calc_tax' => 'per_order', + ]); + } + } +} diff --git a/wp-bridge/includes/class-rg-storefront.php b/wp-bridge/includes/class-rg-storefront.php new file mode 100644 index 0000000..a71bb58 --- /dev/null +++ b/wp-bridge/includes/class-rg-storefront.php @@ -0,0 +1,189 @@ +<?php +if (!defined('ABSPATH')) exit; + +/** + * SEO storefront, server-rendered from RecordGod's /shop API inside the active theme's chrome. + * /records → browse (release-grouped, ?q= ?genre= ?page=) + * /release/{id} → release detail + in-stock copies + JSON-LD MusicAlbum/Product + * Add-to-cart points at /?rg_add=<sku>, which mints the Woo product on the fly (RG_Cart). + */ +class RG_Storefront { + + static function init() { + add_action('init', [__CLASS__, 'rewrites']); + add_filter('query_vars', function ($v) { + return array_merge($v, ['rg_view', 'rg_id']); + }); + add_action('template_redirect', [__CLASS__, 'render']); + } + + static function rewrites() { + add_rewrite_rule('^records/?$', 'index.php?rg_view=records', 'top'); + add_rewrite_rule('^release/([0-9]+)/?', 'index.php?rg_view=release&rg_id=$matches[1]', 'top'); + } + + static function render() { + $view = get_query_var('rg_view'); + if (!$view) return; + + if ($view === 'release') { + $id = (int) get_query_var('rg_id'); + $data = RG_API::get('/shop/release/' . $id, [], 120); + if (!$data || empty($data['release'])) { self::not_found(); return; } + self::head_for_release($data); + get_header(); + self::release_html($data); + get_footer(); + exit; + } + + // browse + $q = isset($_GET['q']) ? sanitize_text_field(wp_unslash($_GET['q'])) : ''; + $genre = isset($_GET['genre']) ? sanitize_text_field(wp_unslash($_GET['genre'])) : ''; + $page = max(1, (int) ($_GET['page'] ?? 1)); + $args = array_filter(['q' => $q, 'genre' => $genre, 'page' => $page]); + $data = RG_API::get('/shop/browse', $args, 60); + self::head_for_browse($q, $genre); + get_header(); + self::browse_html($data ?: ['items' => [], 'total' => 0, 'page' => 1, 'per' => 24], $q, $genre, $page); + get_footer(); + exit; + } + + // --- <head>: title / description / JSON-LD -------------------------------- + + static function head_for_browse($q, $genre) { + $title = trim(($q ?: $genre) ? ($q ?: $genre) . ' — Records' : 'Records') . ' · ' . get_bloginfo('name'); + add_filter('pre_get_document_title', fn() => $title); + add_action('wp_head', function () { + echo '<meta name="description" content="Browse vinyl, CDs and cassettes in stock.">' . "\n"; + }); + } + + static function head_for_release($data) { + $r = $data['release']; + $copies = $data['copies']; + $title = trim($r['artist'] . ' – ' . $r['title']) . ' · ' . get_bloginfo('name'); + add_filter('pre_get_document_title', fn() => $title); + add_action('wp_head', function () use ($r, $copies) { + $price = $copies ? min(array_column($copies, 'price')) : null; + $desc = trim(implode(' · ', array_filter([$r['artist'], $r['format'], $r['year'], $r['label']]))); + echo '<meta name="description" content="' . esc_attr($desc) . '">' . "\n"; + $ld = [ + '@context' => 'https://schema.org', + '@type' => 'MusicAlbum', + 'name' => $r['title'], + 'byArtist' => ['@type' => 'MusicGroup', 'name' => $r['artist']], + ]; + if (!empty($r['thumb'])) $ld['image'] = $r['thumb']; + if (!empty($r['genre'])) $ld['genre'] = $r['genre']; + if ($price !== null) { + $ld['offers'] = [ + '@type' => 'Offer', 'priceCurrency' => 'AUD', 'price' => $price, + 'availability' => $copies ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock', + 'url' => home_url('/release/' . $r['id']), + ]; + } + echo '<script type="application/ld+json">' . wp_json_encode($ld) . '</script>' . "\n"; + }); + } + + // --- body ----------------------------------------------------------------- + + static function browse_html($d, $q, $genre, $page) { + $heading = $q ?: ($genre ?: 'Records'); + echo '<main class="rg-shop" style="max-width:1100px;margin:2rem auto;padding:0 1rem">'; + echo '<form method="get" action="' . esc_url(home_url('/records')) . '" style="margin-bottom:1.5rem">'; + echo '<input type="search" name="q" value="' . esc_attr($q) . '" placeholder="Search artist or title" + style="padding:.6rem;width:60%;max-width:420px"> <button type="submit">Search</button></form>'; + echo '<h1>' . esc_html($heading) . ' <small style="font-weight:400;color:#888">(' . (int) $d['total'] . ')</small></h1>'; + + if (!$d['items']) { echo '<p>Nothing in stock matching that.</p></main>'; return; } + + echo '<div class="rg-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:1.2rem">'; + foreach ($d['items'] as $it) { + $url = home_url('/release/' . (int) $it['release_id']); + echo '<a href="' . esc_url($url) . '" style="text-decoration:none;color:inherit">'; + if (!empty($it['thumb'])) + echo '<img src="' . esc_url($it['thumb']) . '" alt="' . esc_attr($it['title']) . '" loading="lazy" + style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:6px">'; + echo '<div style="font-weight:600;margin-top:.4rem;font-size:.9rem">' . esc_html($it['title']) . '</div>'; + echo '<div style="color:#888;font-size:.85rem">' . esc_html($it['artist']) . '</div>'; + echo '<div style="margin-top:.2rem">$' . esc_html(number_format((float) $it['price'], 2)); + if ((int) $it['copies'] > 1) echo ' <small style="color:#888">· ' . (int) $it['copies'] . ' copies</small>'; + echo '</div></a>'; + } + echo '</div>'; + + self::pager($d, $page, $q, $genre); + echo '</main>'; + } + + static function pager($d, $page, $q, $genre) { + $pages = (int) ceil($d['total'] / max(1, $d['per'])); + if ($pages <= 1) return; + $link = fn($p) => esc_url(add_query_arg(array_filter(['q' => $q, 'genre' => $genre, 'page' => $p]), home_url('/records'))); + echo '<div style="margin:2rem 0;text-align:center">'; + if ($page > 1) echo '<a href="' . $link($page - 1) . '">← Prev</a> '; + echo '<span style="margin:0 1rem">Page ' . $page . ' of ' . $pages . '</span>'; + if ($page < $pages) echo '<a href="' . $link($page + 1) . '">Next →</a>'; + echo '</div>'; + } + + static function release_html($data) { + $r = $data['release']; + echo '<main class="rg-release" style="max-width:900px;margin:2rem auto;padding:0 1rem">'; + echo '<p><a href="' . esc_url(home_url('/records')) . '">← Records</a></p>'; + echo '<div style="display:flex;gap:2rem;flex-wrap:wrap">'; + if (!empty($r['thumb'])) + echo '<img src="' . esc_url($r['thumb']) . '" alt="' . esc_attr($r['title']) . '" + style="width:320px;max-width:100%;border-radius:8px">'; + echo '<div style="flex:1;min-width:280px">'; + echo '<h1 style="margin:0">' . esc_html($r['title']) . '</h1>'; + echo '<h2 style="margin:.2rem 0 1rem;font-weight:400;color:#666">' . esc_html($r['artist']) . '</h2>'; + foreach (['label' => 'Label', 'format' => 'Format', 'country' => 'Country', + 'year' => 'Year', 'genre' => 'Genre', 'style' => 'Style'] as $k => $lbl) + if (!empty($r[$k])) + echo '<div><strong>' . $lbl . ':</strong> ' . esc_html($r[$k]) . '</div>'; + + echo '<h3 style="margin-top:1.5rem">In stock</h3>'; + if (empty($data['copies'])) { + echo '<p>No copies in stock right now.</p>'; + } else { + echo '<ul style="list-style:none;padding:0">'; + foreach ($data['copies'] as $c) { + $cond = trim(implode(' / ', array_filter([$c['condition'], $c['sleeve_cond'] ?? null]))); + echo '<li style="display:flex;justify-content:space-between;align-items:center; + border:1px solid #eee;border-radius:6px;padding:.6rem .8rem;margin-bottom:.5rem">'; + echo '<span>$' . esc_html(number_format((float) $c['price'], 2)); + if ($cond) echo ' <small style="color:#888">· ' . esc_html($cond) . '</small>'; + echo '</span>'; + echo '<a class="button" href="' . esc_url(home_url('/?rg_add=' . rawurlencode($c['sku']))) . '" + style="background:#111;color:#fff;padding:.4rem .9rem;border-radius:5px;text-decoration:none">Add to cart</a>'; + echo '</li>'; + } + echo '</ul>'; + } + echo '</div></div>'; + + if (!empty($data['tracks'])) { + echo '<h3 style="margin-top:2rem">Tracklist</h3><ol style="columns:2;max-width:600px">'; + foreach ($data['tracks'] as $t) { + echo '<li>' . esc_html($t['title']); + if (!empty($t['duration'])) echo ' <small style="color:#999">' . esc_html($t['duration']) . '</small>'; + echo '</li>'; + } + echo '</ol>'; + } + echo '</main>'; + } + + static function not_found() { + status_header(404); + get_header(); + echo '<main style="max-width:700px;margin:3rem auto;text-align:center"><h1>Record not found</h1>' + . '<p><a href="' . esc_url(home_url('/records')) . '">Browse the shop →</a></p></main>'; + get_footer(); + exit; + } +} diff --git a/wp-bridge/recordgod-bridge.php b/wp-bridge/recordgod-bridge.php new file mode 100644 index 0000000..3e3ecc2 --- /dev/null +++ b/wp-bridge/recordgod-bridge.php @@ -0,0 +1,78 @@ +<?php +/** + * Plugin Name: RecordGod Bridge + * Description: Thin bridge to RecordGod — RecordGod owns the catalog/stock/pricing/shipping; this plugin + * renders SEO storefront pages from its /shop API, mints Woo products on the fly at add-to-cart, + * injects postage, and posts completed orders back. Successor to WowPlatter. + * Version: 0.1.0 + * Author: Monster Robot + * Requires Plugins: woocommerce + */ + +if (!defined('ABSPATH')) exit; + +define('RG_BRIDGE_VERSION', '0.1.0'); +define('RG_BRIDGE_DIR', plugin_dir_path(__FILE__)); + +require_once RG_BRIDGE_DIR . 'includes/class-rg-api.php'; +require_once RG_BRIDGE_DIR . 'includes/class-rg-cart.php'; +require_once RG_BRIDGE_DIR . 'includes/class-rg-storefront.php'; +require_once RG_BRIDGE_DIR . 'includes/class-rg-orders.php'; + +// Shipping method loads only once Woo's class exists. +add_action('woocommerce_shipping_init', function () { + require_once RG_BRIDGE_DIR . 'includes/class-rg-shipping.php'; +}); +add_filter('woocommerce_shipping_methods', function ($methods) { + $methods['recordgod'] = 'RG_Shipping'; + return $methods; +}); + +// Wire the pieces. +add_action('plugins_loaded', function () { + RG_Cart::init(); + RG_Storefront::init(); + RG_Orders::init(); +}); + +// --- Settings (Settings API): base URL + bridge key + store id --------------- +add_action('admin_menu', function () { + add_options_page('RecordGod Bridge', 'RecordGod Bridge', 'manage_options', 'rg-bridge', 'rg_bridge_settings_page'); +}); +add_action('admin_init', function () { + register_setting('rg_bridge', 'rg_base_url'); + register_setting('rg_bridge', 'rg_bridge_key'); + register_setting('rg_bridge', 'rg_store_id'); +}); +function rg_bridge_settings_page() { + ?> + <div class="wrap"> + <h1>RecordGod Bridge</h1> + <form method="post" action="options.php"> + <?php settings_fields('rg_bridge'); ?> + <table class="form-table"> + <tr><th>RecordGod base URL</th> + <td><input type="url" name="rg_base_url" class="regular-text" + value="<?php echo esc_attr(get_option('rg_base_url', 'https://recordgod.com')); ?>" + placeholder="https://recordgod.com"></td></tr> + <tr><th>Bridge key</th> + <td><input type="text" name="rg_bridge_key" class="regular-text" + value="<?php echo esc_attr(get_option('rg_bridge_key', '')); ?>"> + <p class="description">Must match the <code>bridge_key</code> secret in RecordGod settings.</p></td></tr> + <tr><th>Store ID</th> + <td><input type="number" name="rg_store_id" class="small-text" + value="<?php echo esc_attr(get_option('rg_store_id', '1')); ?>"></td></tr> + </table> + <?php submit_button(); ?> + </form> + <p><a href="<?php echo esc_url(home_url('/records')); ?>">View storefront →</a></p> + </div> + <?php +} + +// Storefront rewrite rules need flushing on (de)activate. +register_activation_hook(__FILE__, function () { + RG_Storefront::rewrites(); + flush_rewrite_rules(); +}); +register_deactivation_hook(__FILE__, 'flush_rewrite_rules');