@blamejs/blamejs-shop 0.0.128 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +2 -0
- package/lib/admin.js +1 -2
- package/lib/affiliates.js +4 -3
- package/lib/analytics.js +3 -2
- package/lib/api-keys.js +1 -1
- package/lib/assembly-instructions.js +2 -1
- package/lib/auto-replenish.js +4 -3
- package/lib/backorder.js +2 -1
- package/lib/business-hours.js +8 -1
- package/lib/carrier-accounts.js +1 -1
- package/lib/carrier-rates.js +1 -1
- package/lib/cart-abandonment.js +3 -2
- package/lib/cart-bulk-ops.js +2 -1
- package/lib/cart-recovery.js +5 -4
- package/lib/cart.js +6 -2
- package/lib/catalog-drafts.js +1 -1
- package/lib/click-and-collect.js +3 -2
- package/lib/clickstream.js +4 -3
- package/lib/config.js +2 -1
- package/lib/cookie-consent.js +2 -1
- package/lib/credit-limits.js +2 -1
- package/lib/currency-display.js +2 -1
- package/lib/customer-activity.js +3 -2
- package/lib/customer-impersonation.js +3 -3
- package/lib/customer-merge.js +4 -3
- package/lib/customer-portal.js +4 -4
- package/lib/customer-risk-profile.js +2 -1
- package/lib/customer-segments.js +2 -1
- package/lib/customer-surveys.js +6 -3
- package/lib/delivery-estimate.js +2 -2
- package/lib/demand-forecast.js +2 -1
- package/lib/discount-analytics.js +2 -2
- package/lib/dunning.js +4 -1
- package/lib/email-warmup.js +6 -1
- package/lib/email.js +1 -8
- package/lib/error-log.js +3 -2
- package/lib/event-log.js +3 -2
- package/lib/fraud-screen.js +3 -1
- package/lib/fulfillment-sla.js +3 -1
- package/lib/index.js +11 -3
- package/lib/inventory-allocations.js +3 -0
- package/lib/inventory-snapshots.js +2 -1
- package/lib/invoice-renderer.js +2 -1
- package/lib/line-gift-wrap.js +6 -1
- package/lib/live-chat.js +2 -1
- package/lib/loyalty-redemption.js +2 -1
- package/lib/newsletter.js +6 -1
- package/lib/operator-activity-feed.js +4 -3
- package/lib/operator-sessions.js +7 -7
- package/lib/order-exchanges.js +1 -0
- package/lib/order-timeline.js +2 -1
- package/lib/payment-retries.js +2 -1
- package/lib/payment.js +5 -4
- package/lib/pixel-events.js +6 -5
- package/lib/preorder.js +2 -1
- package/lib/print-queue.js +2 -1
- package/lib/product-compare.js +2 -1
- package/lib/product-qa.js +2 -1
- package/lib/push-notifications.js +6 -5
- package/lib/recently-viewed.js +7 -2
- package/lib/recommendations.js +7 -2
- package/lib/referral-leaderboard.js +2 -1
- package/lib/refund-automation.js +1 -1
- package/lib/refund-policy.js +1 -1
- package/lib/reorder-reminders.js +2 -1
- package/lib/reorder-thresholds.js +2 -1
- package/lib/robots-config.js +1 -0
- package/lib/sales-reports.js +17 -14
- package/lib/sales-tax-filings.js +2 -1
- package/lib/save-for-later.js +2 -1
- package/lib/search-suggestions.js +1 -1
- package/lib/shipping-insurance.js +2 -1
- package/lib/shipping-labels.js +3 -2
- package/lib/shipping-zones.js +1 -0
- package/lib/shrinkage-report.js +9 -8
- package/lib/sms-dispatcher.js +6 -5
- package/lib/stock-alerts.js +1 -1
- package/lib/stock-receipts.js +2 -1
- package/lib/store-credit.js +2 -1
- package/lib/storefront-forms.js +1 -1
- package/lib/storefront.js +223 -141
- package/lib/subscription-analytics.js +7 -2
- package/lib/subscription-controls.js +9 -8
- package/lib/subscription-gifts.js +2 -1
- package/lib/subscriptions.js +2 -0
- package/lib/support-tickets.js +4 -4
- package/lib/tax-cert-renewals.js +2 -1
- package/lib/tax-remittance.js +2 -1
- package/lib/theme-assets.js +1 -1
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +122 -2
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/did.js +367 -0
- package/lib/vendor/blamejs/lib/mdoc.js +305 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.40.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.41.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +27 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/did.test.js +147 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mdoc.test.js +230 -0
- package/lib/vendor-invoices.js +1 -1
- package/lib/webhook-receiver.js +8 -2
- package/lib/webhook-subscriptions.js +1 -1
- package/lib/webhooks.js +6 -5
- package/lib/winback-campaigns.js +2 -1
- package/lib/wishlist-alerts.js +2 -1
- package/lib/wishlist-digest.js +2 -1
- package/package.json +1 -1
package/lib/storefront.js
CHANGED
|
@@ -1199,6 +1199,40 @@ function renderCollection(opts) {
|
|
|
1199
1199
|
});
|
|
1200
1200
|
}
|
|
1201
1201
|
|
|
1202
|
+
// Account "Recently viewed" page — a newest-first grid of products the
|
|
1203
|
+
// signed-in customer has opened, reusing the standard product card.
|
|
1204
|
+
// `opts.products` is a resolved [{ slug, title, price, image_url,
|
|
1205
|
+
// image_alt }] list (archived products are dropped before render, so
|
|
1206
|
+
// the grid is orphan-tolerant). A "Clear history" control renders only
|
|
1207
|
+
// when the list is non-empty.
|
|
1208
|
+
function renderRecentlyViewed(opts) {
|
|
1209
|
+
var products = opts.products || [];
|
|
1210
|
+
var cards = products.map(function (p) { return _buildProductCard(p); }).join("");
|
|
1211
|
+
var grid = cards
|
|
1212
|
+
? "<div class=\"catalog-grid recently-viewed-grid\">" + cards + "</div>"
|
|
1213
|
+
: "<p class=\"recently-viewed-empty\">You haven't viewed any products yet. As you browse the shop, the products you open show up here.</p>";
|
|
1214
|
+
var clear = cards
|
|
1215
|
+
? "<form class=\"recently-viewed__clear\" method=\"post\" action=\"/account/recently-viewed/clear\">" +
|
|
1216
|
+
"<button type=\"submit\" class=\"btn-ghost btn-ghost--sm\">Clear history</button></form>"
|
|
1217
|
+
: "";
|
|
1218
|
+
var body =
|
|
1219
|
+
"<section class=\"account-recently-viewed\">" +
|
|
1220
|
+
"<nav class=\"breadcrumb\" aria-label=\"Breadcrumb\"><ol>" +
|
|
1221
|
+
"<li><a href=\"/account\">Account</a></li>" +
|
|
1222
|
+
"<li aria-current=\"page\">Recently viewed</li>" +
|
|
1223
|
+
"</ol></nav>" +
|
|
1224
|
+
"<header class=\"account-recently-viewed__head\">" +
|
|
1225
|
+
"<h1 class=\"account-recently-viewed__title\">Recently viewed</h1>" +
|
|
1226
|
+
clear +
|
|
1227
|
+
"</header>" +
|
|
1228
|
+
grid +
|
|
1229
|
+
"</section>";
|
|
1230
|
+
return _wrap({
|
|
1231
|
+
title: "Recently viewed", shop_name: opts.shop_name || "blamejs.shop",
|
|
1232
|
+
cart_count: opts.cart_count == null ? 0 : opts.cart_count, theme_css: opts.theme_css, body: body,
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1202
1236
|
var RETURN_REASONS = [
|
|
1203
1237
|
["defective", "Defective / doesn't work"],
|
|
1204
1238
|
["wrong-item", "Wrong item received"],
|
|
@@ -1476,9 +1510,9 @@ var CART_LINE =
|
|
|
1476
1510
|
" </span>\n" +
|
|
1477
1511
|
" </a>\n" +
|
|
1478
1512
|
" </td>\n" +
|
|
1479
|
-
" <td>{{qty}}</td>\n" +
|
|
1480
|
-
" <td class=\"price\">{{unit}}</td>\n" +
|
|
1481
|
-
" <td class=\"price\">{{total}}</td>\n" +
|
|
1513
|
+
" <td data-label=\"Qty\">{{qty}}</td>\n" +
|
|
1514
|
+
" <td class=\"price\" data-label=\"Unit\">{{unit}}</td>\n" +
|
|
1515
|
+
" <td class=\"price\" data-label=\"Total\">{{total}}</td>\n" +
|
|
1482
1516
|
"</tr>\n";
|
|
1483
1517
|
|
|
1484
1518
|
// Editable cart line — shown on the /cart page. Includes an inline
|
|
@@ -1502,14 +1536,14 @@ var CART_LINE_EDITABLE =
|
|
|
1502
1536
|
" </span>\n" +
|
|
1503
1537
|
" </a>\n" +
|
|
1504
1538
|
" </td>\n" +
|
|
1505
|
-
" <td class=\"cart-line__qty\">\n" +
|
|
1539
|
+
" <td class=\"cart-line__qty\" data-label=\"Qty\">\n" +
|
|
1506
1540
|
" <form method=\"post\" action=\"/cart/lines/{{line_id}}/update\" class=\"cart-line__update\">\n" +
|
|
1507
|
-
" <input type=\"number\" name=\"qty\" value=\"{{qty}}\" min=\"1\" max=\"
|
|
1508
|
-
" <button type=\"submit\" class=\"cart-line__btn\">Update</button>\n" +
|
|
1541
|
+
" <input type=\"number\" name=\"qty\" value=\"{{qty}}\" min=\"1\" max=\"99999\" class=\"cart-line__qty-input\" aria-label=\"Quantity\">\n" +
|
|
1542
|
+
" <button type=\"submit\" class=\"cart-line__btn cart-line__btn--update\">Update</button>\n" +
|
|
1509
1543
|
" </form>\n" +
|
|
1510
1544
|
" </td>\n" +
|
|
1511
|
-
" <td class=\"price\">{{unit}}</td>\n" +
|
|
1512
|
-
" <td class=\"price\">{{total}}</td>\n" +
|
|
1545
|
+
" <td class=\"price\" data-label=\"Price\">{{unit}}</td>\n" +
|
|
1546
|
+
" <td class=\"price\" data-label=\"Total\">{{total}}</td>\n" +
|
|
1513
1547
|
" <td class=\"cart-line__remove-cell\">\n" +
|
|
1514
1548
|
" RAW_CART_LINE_SAVE" +
|
|
1515
1549
|
" <form method=\"post\" action=\"/cart/lines/{{line_id}}/remove\">\n" +
|
|
@@ -1657,7 +1691,7 @@ var ORDER_PAGE =
|
|
|
1657
1691
|
" <div class=\"order-page__items\">\n" +
|
|
1658
1692
|
" <h2 class=\"pdp__variants-title\">Items</h2>\n" +
|
|
1659
1693
|
" <div class=\"table-scroll\">\n" +
|
|
1660
|
-
" <table>\n" +
|
|
1694
|
+
" <table class=\"cart-table\">\n" +
|
|
1661
1695
|
" <thead><tr><th>Product</th><th>Qty</th><th>Unit</th><th>Total</th></tr></thead>\n" +
|
|
1662
1696
|
" <tbody>{{line_rows}}</tbody>\n" +
|
|
1663
1697
|
" </table>\n" +
|
|
@@ -2048,7 +2082,6 @@ function renderNotFound(opts) {
|
|
|
2048
2082
|
// — it's a routing key, not an authentication token. The cart itself
|
|
2049
2083
|
// transitions to `customer_id` on login via cart.setCustomer.
|
|
2050
2084
|
var SESSION_COOKIE_NAME = "shop_sid";
|
|
2051
|
-
var SESSION_COOKIE_MAX = 60 * 60 * 24 * 30; // 30 days
|
|
2052
2085
|
|
|
2053
2086
|
// Authenticated-customer cookie — carries an opaque sealed envelope
|
|
2054
2087
|
// `{ customer_id, exp }`, AEAD-encrypted via b.vault.seal so the
|
|
@@ -2058,79 +2091,85 @@ var SESSION_COOKIE_MAX = 60 * 60 * 24 * 30; // 30 days
|
|
|
2058
2091
|
// rotated vault invalidates every outstanding auth cookie (operator-
|
|
2059
2092
|
// initiated logout-everywhere).
|
|
2060
2093
|
var AUTH_COOKIE_NAME = "shop_auth";
|
|
2061
|
-
var AUTH_COOKIE_MAX = 60 * 60 * 24 * 14; // 14 days
|
|
2062
|
-
var AUTH_TTL_MS = 14 * 24 * 60 * 60 * 1000;
|
|
2063
2094
|
|
|
2064
2095
|
// WebAuthn ceremony state cookie — short-lived envelope holding the
|
|
2065
2096
|
// random challenge + the ceremony-scoped metadata so register-finish /
|
|
2066
2097
|
// login-finish can verify the same challenge the browser was sent.
|
|
2067
2098
|
// Path-scoped to /account so it never leaks to other routes.
|
|
2068
2099
|
var CHALLENGE_COOKIE_NAME = "shop_auth_chal";
|
|
2069
|
-
var CHALLENGE_COOKIE_MAX = 5 * 60; // 5 minutes
|
|
2070
2100
|
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2101
|
+
// Short-lived cookie carrying the Stripe PaymentIntent client_secret
|
|
2102
|
+
// from POST /checkout to GET /pay/:order_id. Path-scoped to /pay/ +
|
|
2103
|
+
// SameSite=Strict so it's only ever sent to the pay route.
|
|
2104
|
+
var PAY_COOKIE_NAME = "shop_pay";
|
|
2105
|
+
|
|
2106
|
+
// Shape of a valid session id — mirrors cart.js's SESSION_ID_RE.
|
|
2107
|
+
var SID_SHAPE_RE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
2108
|
+
|
|
2109
|
+
// All cookie transport composes the framework's cookie primitive
|
|
2110
|
+
// (`b.cookies`) — RFC 6265 parse/serialize, prefix invariants, and the
|
|
2111
|
+
// vault-sealed read/write helpers — rather than hand-built Set-Cookie
|
|
2112
|
+
// strings and manual header splitting. The jar is memoized; it only
|
|
2113
|
+
// captures the vault reference (seal/unseal run lazily per call), so
|
|
2114
|
+
// building it before vault.init() has completed is safe.
|
|
2115
|
+
var _jar = null;
|
|
2116
|
+
function _cookieJar() {
|
|
2117
|
+
if (!_jar) {
|
|
2118
|
+
_jar = _b().cookies.create({
|
|
2119
|
+
vault: _b().vault,
|
|
2120
|
+
defaults: { httpOnly: true, secure: true, sameSite: "Lax", path: "/" },
|
|
2121
|
+
});
|
|
2084
2122
|
}
|
|
2085
|
-
return
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
function _setSidCookie(res, sid) {
|
|
2089
|
-
var attrs = "Max-Age=" + SESSION_COOKIE_MAX + "; Path=/; HttpOnly; Secure; SameSite=Lax";
|
|
2090
|
-
var header = SESSION_COOKIE_NAME + "=" + encodeURIComponent(sid) + "; " + attrs;
|
|
2091
|
-
if (typeof res.appendHeader === "function") res.appendHeader("Set-Cookie", header);
|
|
2092
|
-
else if (typeof res.setHeader === "function") res.setHeader("Set-Cookie", header);
|
|
2123
|
+
return _jar;
|
|
2093
2124
|
}
|
|
2094
2125
|
|
|
2095
2126
|
function _readCookie(req, name) {
|
|
2096
|
-
|
|
2097
|
-
if (!raw) return null;
|
|
2098
|
-
var parts = raw.split(";");
|
|
2099
|
-
for (var i = 0; i < parts.length; i += 1) {
|
|
2100
|
-
var p = parts[i].trim();
|
|
2101
|
-
var eq = p.indexOf("=");
|
|
2102
|
-
if (eq <= 0) continue;
|
|
2103
|
-
if (p.slice(0, eq) === name) {
|
|
2104
|
-
try { return decodeURIComponent(p.slice(eq + 1)); }
|
|
2105
|
-
catch (_e) { return null; }
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
return null;
|
|
2127
|
+
return _cookieJar().read(req, name);
|
|
2109
2128
|
}
|
|
2110
2129
|
|
|
2111
|
-
function
|
|
2112
|
-
|
|
2113
|
-
|
|
2130
|
+
function _readSidCookie(req) {
|
|
2131
|
+
// A cookie carrying anything but a well-shaped session id (a stale
|
|
2132
|
+
// value from an old deploy, a tampered cookie, garbage) reads as "no
|
|
2133
|
+
// session" rather than reaching cart.bySession — which throws on a
|
|
2134
|
+
// malformed id and would turn every page that renders the cart count
|
|
2135
|
+
// into a 500. The cookie grants zero authority, so dropping a
|
|
2136
|
+
// malformed one silently is safe.
|
|
2137
|
+
var v = _cookieJar().read(req, SESSION_COOKIE_NAME);
|
|
2138
|
+
return v && SID_SHAPE_RE.test(v) ? v : null;
|
|
2114
2139
|
}
|
|
2115
2140
|
|
|
2116
|
-
function
|
|
2117
|
-
var
|
|
2118
|
-
|
|
2141
|
+
function _setSidCookie(res, sid) {
|
|
2142
|
+
var T = _b().constants.TIME;
|
|
2143
|
+
_cookieJar().write(res, SESSION_COOKIE_NAME, sid, { expires: new Date(Date.now() + T.days(30)) });
|
|
2119
2144
|
}
|
|
2120
2145
|
|
|
2146
|
+
// Auth + WebAuthn-challenge cookies carry a vault-sealed JSON envelope.
|
|
2147
|
+
// writeSealed/readSealed handle the seal + the on-wire prefix; the
|
|
2148
|
+
// caller works in plain objects.
|
|
2149
|
+
function _setAuthCookie(res, env) {
|
|
2150
|
+
var T = _b().constants.TIME;
|
|
2151
|
+
_cookieJar().writeSealed(res, AUTH_COOKIE_NAME, JSON.stringify(env), { expires: new Date(Date.now() + T.days(14)) });
|
|
2152
|
+
}
|
|
2121
2153
|
function _clearAuthCookie(res) {
|
|
2122
|
-
|
|
2123
|
-
AUTH_COOKIE_NAME + "=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Lax");
|
|
2154
|
+
_cookieJar().clear(res, AUTH_COOKIE_NAME);
|
|
2124
2155
|
}
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2156
|
+
function _readAuthEnv(req) {
|
|
2157
|
+
var raw = _cookieJar().readSealed(req, AUTH_COOKIE_NAME);
|
|
2158
|
+
if (raw === null) return null;
|
|
2159
|
+
try { return JSON.parse(raw); } catch (_e) { return null; }
|
|
2129
2160
|
}
|
|
2130
2161
|
|
|
2162
|
+
function _setChallengeCookie(res, env) {
|
|
2163
|
+
var T = _b().constants.TIME;
|
|
2164
|
+
_cookieJar().writeSealed(res, CHALLENGE_COOKIE_NAME, JSON.stringify(env), { expires: new Date(Date.now() + T.minutes(5)), path: "/account" });
|
|
2165
|
+
}
|
|
2131
2166
|
function _clearChallengeCookie(res) {
|
|
2132
|
-
|
|
2133
|
-
|
|
2167
|
+
_cookieJar().clear(res, CHALLENGE_COOKIE_NAME, { path: "/account" });
|
|
2168
|
+
}
|
|
2169
|
+
function _readChallengeEnv(req) {
|
|
2170
|
+
var raw = _cookieJar().readSealed(req, CHALLENGE_COOKIE_NAME);
|
|
2171
|
+
if (raw === null) return null;
|
|
2172
|
+
try { return JSON.parse(raw); } catch (_e) { return null; }
|
|
2134
2173
|
}
|
|
2135
2174
|
|
|
2136
2175
|
// ---- account-page renderers --------------------------------------------
|
|
@@ -2244,6 +2283,7 @@ var ACCOUNT_DASH_PAGE =
|
|
|
2244
2283
|
" <div class=\"account-dash__actions\">\n" +
|
|
2245
2284
|
" <a class=\"btn-secondary\" href=\"/account/wishlist\">Wishlist</a>\n" +
|
|
2246
2285
|
" <a class=\"btn-secondary\" href=\"/account/saved\">Saved for later</a>\n" +
|
|
2286
|
+
" <a class=\"btn-secondary\" href=\"/account/recently-viewed\">Recently viewed</a>\n" +
|
|
2247
2287
|
" <a class=\"btn-secondary\" href=\"/account/addresses\">Addresses</a>\n" +
|
|
2248
2288
|
" <a class=\"btn-secondary\" href=\"/account/returns\">Returns</a>\n" +
|
|
2249
2289
|
" <form method=\"post\" action=\"/account/logout\"><button type=\"submit\" class=\"btn-ghost\">Sign out</button></form>\n" +
|
|
@@ -2268,10 +2308,10 @@ var ACCOUNT_DASH_PAGE =
|
|
|
2268
2308
|
|
|
2269
2309
|
var ACCOUNT_DASH_ORDER_ROW =
|
|
2270
2310
|
"<tr>\n" +
|
|
2271
|
-
" <td><a href=\"/orders/{{order_id}}\" class=\"account-order__id\"><code>{{order_id_short}}</code></a></td>\n" +
|
|
2272
|
-
" <td class=\"account-order__items\">RAW_ACCOUNT_ORDER_THUMBS</td>\n" +
|
|
2273
|
-
" <td><span class=\"pdp__badge {{status_class}}\">{{status}}</span></td>\n" +
|
|
2274
|
-
" <td class=\"price\">{{total}}</td>\n" +
|
|
2311
|
+
" <td data-label=\"Order\"><a href=\"/orders/{{order_id}}\" class=\"account-order__id\"><code>{{order_id_short}}</code></a></td>\n" +
|
|
2312
|
+
" <td class=\"account-order__items\" data-label=\"Items\">RAW_ACCOUNT_ORDER_THUMBS</td>\n" +
|
|
2313
|
+
" <td data-label=\"Status\"><span class=\"pdp__badge {{status_class}}\">{{status}}</span></td>\n" +
|
|
2314
|
+
" <td class=\"price\" data-label=\"Total\">{{total}}</td>\n" +
|
|
2275
2315
|
"</tr>\n";
|
|
2276
2316
|
|
|
2277
2317
|
function renderAccount(opts) {
|
|
@@ -2372,7 +2412,7 @@ function mount(router, deps) {
|
|
|
2372
2412
|
// so the /admin landing + the onNotFound 404 handler (mounted
|
|
2373
2413
|
// outside the `if (deps.customers)` block below) can reach it.
|
|
2374
2414
|
async function _cartCountForReq(req) {
|
|
2375
|
-
var sid =
|
|
2415
|
+
var sid = _readSidCookie(req);
|
|
2376
2416
|
if (!sid) return 0;
|
|
2377
2417
|
var c = await deps.cart.bySession(sid);
|
|
2378
2418
|
if (!c) return 0;
|
|
@@ -2380,6 +2420,42 @@ function mount(router, deps) {
|
|
|
2380
2420
|
return lines.length;
|
|
2381
2421
|
}
|
|
2382
2422
|
|
|
2423
|
+
// The signed-in customer's sealed-cookie envelope, or null. Shared by
|
|
2424
|
+
// the PDP view recorder (mounted outside the `if (deps.customers)`
|
|
2425
|
+
// block) and the account routes inside it, so there's one auth-cookie
|
|
2426
|
+
// reader rather than a copy per call site. A missing / malformed /
|
|
2427
|
+
// expired cookie returns null — never throws.
|
|
2428
|
+
function _currentCustomerEnv(req) {
|
|
2429
|
+
var env = _readAuthEnv(req);
|
|
2430
|
+
if (!env || !env.customer_id || !env.exp || env.exp < Date.now()) return null;
|
|
2431
|
+
return env;
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
// Resolve a product id into the { slug, title, price, image_url,
|
|
2435
|
+
// image_alt } shape `_buildProductCard` expects. Returns null for an
|
|
2436
|
+
// archived / missing product so it drops out of any grid (collections,
|
|
2437
|
+
// recently-viewed). Shared so the decoration rule lives in one place.
|
|
2438
|
+
var _cardAssetPrefix = deps.asset_prefix || "/assets/";
|
|
2439
|
+
async function _decorateProductCard(pid) {
|
|
2440
|
+
var product = await deps.catalog.products.get(pid);
|
|
2441
|
+
if (!product || product.status !== "active") return null;
|
|
2442
|
+
var priceStr = "—";
|
|
2443
|
+
var variants = await deps.catalog.variants.listForProduct(pid);
|
|
2444
|
+
if (variants.length) {
|
|
2445
|
+
var pr = await deps.catalog.prices.current(variants[0].id, "USD");
|
|
2446
|
+
if (pr) priceStr = pricing.format(pr.amount_minor, pr.currency);
|
|
2447
|
+
}
|
|
2448
|
+
var media = await deps.catalog.media.listForProduct(pid);
|
|
2449
|
+
var hero = media.length ? media[0] : null;
|
|
2450
|
+
return {
|
|
2451
|
+
slug: product.slug,
|
|
2452
|
+
title: product.title,
|
|
2453
|
+
price: priceStr,
|
|
2454
|
+
image_url: hero ? (_cardAssetPrefix + hero.r2_key) : null,
|
|
2455
|
+
image_alt: hero ? (hero.alt_text || product.title) : null,
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2383
2459
|
// Resolve the cart for this request — read session_id from the
|
|
2384
2460
|
// sealed cookie, create one (and the cart) if absent. Returns
|
|
2385
2461
|
// the cart row OR null when the cart was just created (caller can
|
|
@@ -2520,6 +2596,19 @@ function mount(router, deps) {
|
|
|
2520
2596
|
try { wishlistCount = await deps.wishlist.countForProduct(product.id); }
|
|
2521
2597
|
catch (_e) { wishlistCount = 0; }
|
|
2522
2598
|
}
|
|
2599
|
+
// Log the view for a signed-in customer so it surfaces on their
|
|
2600
|
+
// "Recently viewed" account page. Drop-silent — a recording failure
|
|
2601
|
+
// (table not migrated, write contention) must never break the PDP
|
|
2602
|
+
// render. Guests aren't recorded here: their PDP is edge-cached with
|
|
2603
|
+
// no per-request container hop, so session-scoped guest history is a
|
|
2604
|
+
// separate opt-in (a client beacon) the storefront doesn't ship yet.
|
|
2605
|
+
if (deps.recentlyViewed && deps.customers) {
|
|
2606
|
+
var rvEnv = _currentCustomerEnv(req);
|
|
2607
|
+
if (rvEnv) {
|
|
2608
|
+
try { await deps.recentlyViewed.recordView({ customer_id: rvEnv.customer_id, product_id: product.id }); }
|
|
2609
|
+
catch (_e) { /* drop-silent — recently-viewed is supplementary to the buy path */ }
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2523
2612
|
var html = renderProduct({
|
|
2524
2613
|
product: product,
|
|
2525
2614
|
variants: variants,
|
|
@@ -2539,36 +2628,11 @@ function mount(router, deps) {
|
|
|
2539
2628
|
// Collections — operator-curated + smart product lists. Public browse
|
|
2540
2629
|
// pages; mounted when the collections primitive is wired.
|
|
2541
2630
|
if (deps.collections) {
|
|
2542
|
-
var _collAssetPrefix = deps.asset_prefix || "/assets/";
|
|
2543
|
-
|
|
2544
|
-
// Decorate a product id into the { slug, title, price, image_url }
|
|
2545
|
-
// shape _buildProductCard expects. Returns null for an archived /
|
|
2546
|
-
// missing product so it drops out of the grid.
|
|
2547
|
-
async function _decorateCollectionProduct(pid) {
|
|
2548
|
-
var product = await deps.catalog.products.get(pid);
|
|
2549
|
-
if (!product || product.status !== "active") return null;
|
|
2550
|
-
var priceStr = "—";
|
|
2551
|
-
var variants = await deps.catalog.variants.listForProduct(pid);
|
|
2552
|
-
if (variants.length) {
|
|
2553
|
-
var pr = await deps.catalog.prices.current(variants[0].id, "USD");
|
|
2554
|
-
if (pr) priceStr = pricing.format(pr.amount_minor, pr.currency);
|
|
2555
|
-
}
|
|
2556
|
-
var media = await deps.catalog.media.listForProduct(pid);
|
|
2557
|
-
var hero = media.length ? media[0] : null;
|
|
2558
|
-
return {
|
|
2559
|
-
slug: product.slug,
|
|
2560
|
-
title: product.title,
|
|
2561
|
-
price: priceStr,
|
|
2562
|
-
image_url: hero ? (_collAssetPrefix + hero.r2_key) : null,
|
|
2563
|
-
image_alt: hero ? (hero.alt_text || product.title) : null,
|
|
2564
|
-
};
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
2631
|
router.get("/collections", async function (req, res) {
|
|
2568
2632
|
var cols = await deps.collections.list({ active_only: true });
|
|
2569
2633
|
var cartCount = await _cartCountForReq(req);
|
|
2570
2634
|
_send(res, 200, renderCollectionList({
|
|
2571
|
-
collections: cols, shop_name: shopName, cart_count: cartCount, asset_prefix:
|
|
2635
|
+
collections: cols, shop_name: shopName, cart_count: cartCount, asset_prefix: _cardAssetPrefix,
|
|
2572
2636
|
}));
|
|
2573
2637
|
});
|
|
2574
2638
|
|
|
@@ -2589,7 +2653,7 @@ function mount(router, deps) {
|
|
|
2589
2653
|
var products = [];
|
|
2590
2654
|
for (var i = 0; i < result.rows.length; i += 1) {
|
|
2591
2655
|
var pid = result.rows[i].product_id || result.rows[i].id;
|
|
2592
|
-
var card = await
|
|
2656
|
+
var card = await _decorateProductCard(pid);
|
|
2593
2657
|
if (card) products.push(card);
|
|
2594
2658
|
}
|
|
2595
2659
|
var cartCount = await _cartCountForReq(req);
|
|
@@ -2713,11 +2777,11 @@ function mount(router, deps) {
|
|
|
2713
2777
|
idempotency_key: "checkout:" + c.id + ":" + _b().uuid.v7(),
|
|
2714
2778
|
});
|
|
2715
2779
|
// Set a short-lived pay cookie so /pay/:order_id can serve the
|
|
2716
|
-
// client_secret without re-running confirm.
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2780
|
+
// client_secret without re-running confirm. Scoped to /pay/ +
|
|
2781
|
+
// SameSite=Strict so it's only ever sent to the pay route.
|
|
2782
|
+
_cookieJar().write(res, PAY_COOKIE_NAME, result.payment_intent.client_secret, {
|
|
2783
|
+
expires: new Date(Date.now() + _b().constants.TIME.minutes(15)), path: "/pay/", sameSite: "Strict",
|
|
2784
|
+
});
|
|
2721
2785
|
res.status(303);
|
|
2722
2786
|
res.setHeader && res.setHeader("location", "/pay/" + result.order.id);
|
|
2723
2787
|
return res.end ? res.end() : res.send("");
|
|
@@ -2736,14 +2800,7 @@ function mount(router, deps) {
|
|
|
2736
2800
|
// Read the client_secret from the shop_pay cookie set on POST
|
|
2737
2801
|
// /checkout. The cookie is scoped Path=/pay/ + SameSite=Strict
|
|
2738
2802
|
// so it's only sent to the pay route and never cross-origin.
|
|
2739
|
-
var
|
|
2740
|
-
var clientSecret = null;
|
|
2741
|
-
rawCookies.split(";").forEach(function (p) {
|
|
2742
|
-
var t = p.trim();
|
|
2743
|
-
if (t.indexOf("shop_pay=") === 0) {
|
|
2744
|
-
try { clientSecret = decodeURIComponent(t.slice("shop_pay=".length)); } catch (_e) { /* drop */ }
|
|
2745
|
-
}
|
|
2746
|
-
});
|
|
2803
|
+
var clientSecret = _readCookie(req, PAY_COOKIE_NAME);
|
|
2747
2804
|
if (!clientSecret) {
|
|
2748
2805
|
res.status(303); res.setHeader && res.setHeader("location", "/cart");
|
|
2749
2806
|
return res.end ? res.end() : res.send("");
|
|
@@ -2809,23 +2866,8 @@ function mount(router, deps) {
|
|
|
2809
2866
|
return _b().crypto.toBase64Url(buf);
|
|
2810
2867
|
}
|
|
2811
2868
|
|
|
2812
|
-
function _sealEnvelope(obj) {
|
|
2813
|
-
return _b().vault.seal(JSON.stringify(obj));
|
|
2814
|
-
}
|
|
2815
|
-
function _unsealEnvelope(s) {
|
|
2816
|
-
try {
|
|
2817
|
-
var raw = _b().vault.unseal(s);
|
|
2818
|
-
return JSON.parse(raw);
|
|
2819
|
-
} catch (_e) { return null; }
|
|
2820
|
-
}
|
|
2821
|
-
|
|
2822
2869
|
function _currentCustomer(req) {
|
|
2823
|
-
|
|
2824
|
-
if (!raw) return null;
|
|
2825
|
-
var env = _unsealEnvelope(raw);
|
|
2826
|
-
if (!env || !env.customer_id || !env.exp) return null;
|
|
2827
|
-
if (env.exp < Date.now()) return null;
|
|
2828
|
-
return env;
|
|
2870
|
+
return _currentCustomerEnv(req);
|
|
2829
2871
|
}
|
|
2830
2872
|
|
|
2831
2873
|
function _serviceUnavailable(res, msg) {
|
|
@@ -2883,13 +2925,12 @@ function mount(router, deps) {
|
|
|
2883
2925
|
// Seal the ceremony state (challenge + customer_id) into the
|
|
2884
2926
|
// shop_auth_chal cookie so register-finish verifies against
|
|
2885
2927
|
// the same challenge without server-side state.
|
|
2886
|
-
|
|
2928
|
+
_setChallengeCookie(res, {
|
|
2887
2929
|
kind: "register",
|
|
2888
2930
|
customer_id: customer.id,
|
|
2889
2931
|
challenge: startOpts.challenge,
|
|
2890
2932
|
created_at: Date.now(),
|
|
2891
2933
|
});
|
|
2892
|
-
_setChallengeCookie(res, sealed);
|
|
2893
2934
|
res.status(200);
|
|
2894
2935
|
res.setHeader && res.setHeader("content-type", "application/json");
|
|
2895
2936
|
return res.end ? res.end(JSON.stringify(startOpts)) : res.send(JSON.stringify(startOpts));
|
|
@@ -2902,10 +2943,9 @@ function mount(router, deps) {
|
|
|
2902
2943
|
|
|
2903
2944
|
router.post("/account/passkey/register-finish", async function (req, res) {
|
|
2904
2945
|
try {
|
|
2905
|
-
var
|
|
2906
|
-
if (!
|
|
2907
|
-
|
|
2908
|
-
if (!env || env.kind !== "register") {
|
|
2946
|
+
var env = _readChallengeEnv(req);
|
|
2947
|
+
if (!env) { res.status(400); return res.end ? res.end("missing challenge") : res.send("missing challenge"); }
|
|
2948
|
+
if (env.kind !== "register") {
|
|
2909
2949
|
res.status(400); return res.end ? res.end("bad challenge") : res.send("bad challenge");
|
|
2910
2950
|
}
|
|
2911
2951
|
var att = _readJsonBody(req);
|
|
@@ -2934,10 +2974,10 @@ function mount(router, deps) {
|
|
|
2934
2974
|
transports: transports,
|
|
2935
2975
|
});
|
|
2936
2976
|
_clearChallengeCookie(res);
|
|
2937
|
-
_setAuthCookie(res,
|
|
2977
|
+
_setAuthCookie(res, {
|
|
2938
2978
|
customer_id: env.customer_id,
|
|
2939
|
-
exp: Date.now() +
|
|
2940
|
-
})
|
|
2979
|
+
exp: Date.now() + _b().constants.TIME.days(14),
|
|
2980
|
+
});
|
|
2941
2981
|
res.status(200);
|
|
2942
2982
|
return res.end ? res.end("ok") : res.send("ok");
|
|
2943
2983
|
} catch (e) {
|
|
@@ -2972,13 +3012,12 @@ function mount(router, deps) {
|
|
|
2972
3012
|
allowCredentials: allow,
|
|
2973
3013
|
userVerification: "preferred",
|
|
2974
3014
|
});
|
|
2975
|
-
|
|
3015
|
+
_setChallengeCookie(res, {
|
|
2976
3016
|
kind: "login",
|
|
2977
3017
|
email_hash: hash,
|
|
2978
3018
|
challenge: startOpts.challenge,
|
|
2979
3019
|
created_at: Date.now(),
|
|
2980
3020
|
});
|
|
2981
|
-
_setChallengeCookie(res, sealed);
|
|
2982
3021
|
res.status(200);
|
|
2983
3022
|
res.setHeader && res.setHeader("content-type", "application/json");
|
|
2984
3023
|
return res.end ? res.end(JSON.stringify(startOpts)) : res.send(JSON.stringify(startOpts));
|
|
@@ -2991,10 +3030,9 @@ function mount(router, deps) {
|
|
|
2991
3030
|
|
|
2992
3031
|
router.post("/account/passkey/login-finish", async function (req, res) {
|
|
2993
3032
|
try {
|
|
2994
|
-
var
|
|
2995
|
-
if (!
|
|
2996
|
-
|
|
2997
|
-
if (!env || env.kind !== "login") { res.status(400); return res.end ? res.end("bad challenge") : res.send("bad challenge"); }
|
|
3033
|
+
var env = _readChallengeEnv(req);
|
|
3034
|
+
if (!env) { res.status(400); return res.end ? res.end("missing challenge") : res.send("missing challenge"); }
|
|
3035
|
+
if (env.kind !== "login") { res.status(400); return res.end ? res.end("bad challenge") : res.send("bad challenge"); }
|
|
2998
3036
|
var assertion = _readJsonBody(req);
|
|
2999
3037
|
var credentialId = assertion.id || assertion.rawId;
|
|
3000
3038
|
if (!credentialId) { res.status(400); return res.end ? res.end("missing credential id") : res.send("missing credential id"); }
|
|
@@ -3033,7 +3071,7 @@ function mount(router, deps) {
|
|
|
3033
3071
|
}
|
|
3034
3072
|
// Merge the anonymous cart into a customer-owned cart so
|
|
3035
3073
|
// the shopper doesn't lose items on sign-in.
|
|
3036
|
-
var sid =
|
|
3074
|
+
var sid = _readSidCookie(req);
|
|
3037
3075
|
if (sid) {
|
|
3038
3076
|
try {
|
|
3039
3077
|
var anonCart = await deps.cart.bySession(sid);
|
|
@@ -3041,10 +3079,10 @@ function mount(router, deps) {
|
|
|
3041
3079
|
} catch (_e) { /* best-effort merge; sign-in itself succeeds */ }
|
|
3042
3080
|
}
|
|
3043
3081
|
_clearChallengeCookie(res);
|
|
3044
|
-
_setAuthCookie(res,
|
|
3082
|
+
_setAuthCookie(res, {
|
|
3045
3083
|
customer_id: customer.id,
|
|
3046
|
-
exp: Date.now() +
|
|
3047
|
-
})
|
|
3084
|
+
exp: Date.now() + _b().constants.TIME.days(14),
|
|
3085
|
+
});
|
|
3048
3086
|
res.status(200);
|
|
3049
3087
|
return res.end ? res.end("ok") : res.send("ok");
|
|
3050
3088
|
} catch (e) {
|
|
@@ -3553,6 +3591,50 @@ function mount(router, deps) {
|
|
|
3553
3591
|
});
|
|
3554
3592
|
}
|
|
3555
3593
|
|
|
3594
|
+
// Recently viewed — the signed-in customer's newest-first browse
|
|
3595
|
+
// history. Views are recorded server-side on the (container-rendered)
|
|
3596
|
+
// PDP; this surface lets the customer review + clear that history.
|
|
3597
|
+
if (deps.recentlyViewed) {
|
|
3598
|
+
function _rvAuth(req, res) {
|
|
3599
|
+
var auth;
|
|
3600
|
+
try { auth = _currentCustomer(req); }
|
|
3601
|
+
catch (e) {
|
|
3602
|
+
if (e && e.code === "vault/not-initialized") { _serviceUnavailable(res, "auth not configured"); return null; }
|
|
3603
|
+
throw e;
|
|
3604
|
+
}
|
|
3605
|
+
if (!auth) {
|
|
3606
|
+
res.status(303); res.setHeader && res.setHeader("location", "/account/login");
|
|
3607
|
+
res.end ? res.end() : res.send("");
|
|
3608
|
+
return null;
|
|
3609
|
+
}
|
|
3610
|
+
return auth;
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
router.get("/account/recently-viewed", async function (req, res) {
|
|
3614
|
+
var auth = _rvAuth(req, res); if (!auth) return;
|
|
3615
|
+
// A read failure (table not migrated) degrades to the empty
|
|
3616
|
+
// state rather than 500-ing the account page.
|
|
3617
|
+
var rows = [];
|
|
3618
|
+
try { rows = await deps.recentlyViewed.forCustomer(auth.customer_id, { limit: 24 }); }
|
|
3619
|
+
catch (_e) { rows = []; }
|
|
3620
|
+
var products = [];
|
|
3621
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
3622
|
+
var card = await _decorateProductCard(rows[i].product_id);
|
|
3623
|
+
if (card) products.push(card);
|
|
3624
|
+
}
|
|
3625
|
+
var cartCount = await _cartCountForReq(req);
|
|
3626
|
+
_send(res, 200, renderRecentlyViewed({ products: products, shop_name: shopName, cart_count: cartCount }));
|
|
3627
|
+
});
|
|
3628
|
+
|
|
3629
|
+
router.post("/account/recently-viewed/clear", async function (req, res) {
|
|
3630
|
+
var auth = _rvAuth(req, res); if (!auth) return;
|
|
3631
|
+
try { await deps.recentlyViewed.purgeCustomer(auth.customer_id); }
|
|
3632
|
+
catch (_e) { /* drop-silent — a failed clear leaves history intact, no error surface needed */ }
|
|
3633
|
+
res.status(303); res.setHeader && res.setHeader("location", "/account/recently-viewed");
|
|
3634
|
+
return res.end ? res.end() : res.send("");
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3556
3638
|
// Product reviews — submission requires a logged-in customer AND a
|
|
3557
3639
|
// verified purchase of the product (the gate, not just a badge).
|
|
3558
3640
|
// Only mounts when both the reviews primitive and an order handle
|
|
@@ -162,10 +162,15 @@
|
|
|
162
162
|
|
|
163
163
|
// ---- constants ----------------------------------------------------------
|
|
164
164
|
|
|
165
|
+
// `_b()` is a hoisted function declaration (defined below); resolving the
|
|
166
|
+
// framework constants here at module-eval is safe — the index entry point
|
|
167
|
+
// exposes `framework` before the require cascade.
|
|
168
|
+
var C = _b().constants;
|
|
169
|
+
|
|
165
170
|
var CACHE_NAMESPACE = "subscription-analytics-cache";
|
|
166
171
|
|
|
167
|
-
var DEFAULT_TTL_MS = 5
|
|
168
|
-
var ONE_DAY_MS =
|
|
172
|
+
var DEFAULT_TTL_MS = C.TIME.minutes(5); // 5 minutes
|
|
173
|
+
var ONE_DAY_MS = C.TIME.days(1);
|
|
169
174
|
var ONE_YEAR_MS = 365 * ONE_DAY_MS;
|
|
170
175
|
var DEFAULT_LTV_WINDOW_MS = 90 * ONE_DAY_MS;
|
|
171
176
|
|
|
@@ -55,6 +55,7 @@ function _b() {
|
|
|
55
55
|
if (!bShop) bShop = require("./index");
|
|
56
56
|
return bShop.framework;
|
|
57
57
|
}
|
|
58
|
+
var C = _b().constants;
|
|
58
59
|
|
|
59
60
|
// ---- constants ----------------------------------------------------------
|
|
60
61
|
|
|
@@ -89,12 +90,12 @@ var FREQUENCIES = [
|
|
|
89
90
|
// step skipNext / changeFrequency surface; a calendar-accurate
|
|
90
91
|
// scheduler is out of scope for v1.
|
|
91
92
|
var PERIOD_MS = Object.freeze({
|
|
92
|
-
weekly: 7
|
|
93
|
-
biweekly: 14
|
|
94
|
-
monthly: 30
|
|
95
|
-
quarterly: 90
|
|
96
|
-
semiannual: 182
|
|
97
|
-
annual: 365
|
|
93
|
+
weekly: C.TIME.days(7),
|
|
94
|
+
biweekly: C.TIME.days(14),
|
|
95
|
+
monthly: C.TIME.days(30),
|
|
96
|
+
quarterly: C.TIME.days(90),
|
|
97
|
+
semiannual: C.TIME.days(182),
|
|
98
|
+
annual: C.TIME.days(365),
|
|
98
99
|
});
|
|
99
100
|
|
|
100
101
|
// Plan-interval → frequency fallback. When the row has no `frequency`
|
|
@@ -115,7 +116,7 @@ function _frequencyFromPlan(plan) {
|
|
|
115
116
|
return "monthly";
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
var REACTIVATE_GRACE_MS = 90
|
|
119
|
+
var REACTIVATE_GRACE_MS = C.TIME.days(90);
|
|
119
120
|
var MAX_REASON_LEN = 280;
|
|
120
121
|
var MAX_SKIP_COUNT = 12;
|
|
121
122
|
var MAX_QUANTITY = 1000000;
|
|
@@ -599,7 +600,7 @@ function create(opts) {
|
|
|
599
600
|
if (now - row.cancelled_at > REACTIVATE_GRACE_MS) {
|
|
600
601
|
var gErr = new Error(
|
|
601
602
|
"subscriptionControls.reactivate: refused — cancellation is older than the " +
|
|
602
|
-
(REACTIVATE_GRACE_MS / (
|
|
603
|
+
(REACTIVATE_GRACE_MS / C.TIME.days(1)) + "-day grace window"
|
|
603
604
|
);
|
|
604
605
|
gErr.code = "SUBSCRIPTION_REACTIVATE_GRACE_EXPIRED";
|
|
605
606
|
throw gErr;
|
|
@@ -72,6 +72,7 @@ function _b() {
|
|
|
72
72
|
if (!bShop) bShop = require("./index");
|
|
73
73
|
return bShop.framework;
|
|
74
74
|
}
|
|
75
|
+
var C = _b().constants;
|
|
75
76
|
|
|
76
77
|
// ---- constants ----------------------------------------------------------
|
|
77
78
|
|
|
@@ -94,7 +95,7 @@ var MAX_LIST_LIMIT = 200;
|
|
|
94
95
|
var DEFAULT_LIST_LIMIT = 50;
|
|
95
96
|
// Default redemption window: 365 days. Operators wanting a different
|
|
96
97
|
// horizon pass `expires_at_ms` (absolute) at purchase time.
|
|
97
|
-
var DEFAULT_EXPIRY_MS = 365
|
|
98
|
+
var DEFAULT_EXPIRY_MS = C.TIME.days(365);
|
|
98
99
|
|
|
99
100
|
// ---- validators ---------------------------------------------------------
|
|
100
101
|
|