domma-cms 0.112.1 → 0.113.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/admin/js/app.js +5 -5
- package/package.json +1 -1
- package/plugins/free-tier.lock.json +10 -10
- package/plugins/shopping-cart/CLAUDE.md +15 -0
- package/plugins/shopping-cart/lib/pricing.js +2 -1
- package/plugins/shopping-cart/lib/shop.js +5 -1
- package/plugins/shopping-cart/plugin.js +117 -47
- package/plugins/shopping-cart/plugin.json +1 -1
- package/plugins/shopping-cart/plugin.public.js +2 -2
- package/plugins/shopping-cart/public/lib/products.js +6 -1
- package/plugins/shopping-cart/tests/api.test.js +42 -0
package/admin/js/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import{views as q}from"./views/index.js";import{withPluginChrome as B}from"./lib/plugin-chrome.js";import{api as r,getUser as O,isAuthenticated as u,logout as G,sendToLogin as z}from"./api.js";import{isAuthScreen as C,loginHashFor as K}from"./lib/login-arrange.js";import{installHttpInterceptor as J}from"./http-interceptor.js";import{installResizableSlideovers as Y}from"./lib/slideover-resizable.js";import{installActivityThrobber as Q}from"./lib/activity-throbber.js";import{isPathAllowed as X,redirectFor as Z}from"./lib/admin-scope.js";import{deliverDesktop as tt,desktopActive as et,notificationSettings as it}from"./lib/desktop-notify.js";import{avatarUrl as ot}from"./lib/avatar.js";import{installCalculator as at}from"./lib/calculator.js";$(()=>{J(),at(),Q(),Y(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:e}=await import("./lib/admin-theme.js");u()?await e(r,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let c=null,l="/",h=!1,w=null;function D(t){if(y[t])return E.toast(`${y[t]} is switched off on this site.`,{type:"info"}),"/";const e=b[t]||Z(c,t);return e||((t.split("?")[0]||"/")==="/"&&l&&l!=="/"?b[l]||l:null)}const b={},y={};R.use(async(t,e,i)=>{if(C(t.path))return i();if(!u()){z();return}const o=D(t.path);if(o){R.navigate(o);return}i()});async function g(){try{const t=await r.get("/auth/permissions");c=t.adminScope||null,l=t.adminHome||"/";const e=t.permissions||[];return h=X(c,"/settings")&&e.some(i=>i==="settings"||i.startsWith("settings.")),w=e.some(i=>i==="notifications"||i.startsWith("notifications.")),e}catch{return c=null,l="/",h=!1,w=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{g()});let P=null,N=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(P){N=!0;return}P=(async()=>{do{N=!1;try{await j(await g())}catch{}}while(N)})().finally(()=>{P=null})});async function j(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t,adminScope:c})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(d&&(clearInterval(d),d=null),c=null,l="/",h=!1,w=null)});const F=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Plugin - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:e})=>{if(!C(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),C(e?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(r)),await x();const i=await g(),o=D(t.path);if(o&&R.navigate(o),j(i),it.read().signInToasts)try{const a=await r.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const f=p(s.data?.title||""),k=p((s.data?.body||"").slice(0,120));E.toast(`${f} - ${k}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}T()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const V=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function W(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const e=t?String(t.path).split("?")[0]:"";if(!t||!W(e))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(e))return;let a=0;const n=60,s=1500,f=()=>{a+=n;for(const k of V){const v=document.querySelector(k);if(!v)continue;if(Array.from(v.options||[]).find(H=>H.value===o)){v.value=o,v.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(f,n)};setTimeout(f,n)});const I="cms_card_states",A=S.get(I)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&A[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const e=$(this).closest(".card");e.toggleClass("card-collapsed");const i=$(this).find("h2, h3").first().text().trim();i&&(A[i]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(I,A))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const m={...q},_={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},dashboard:{displayName:"Dashboard",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},blockEditor:{displayName:"Blocks",section:"Edit",icon:"box"},componentEditor:{displayName:"Components",section:"Edit",icon:"component"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,e]of Object.entries(_))m[t]&&(m[t]=B(m[t],{plugin:t,licence:"Built in",...e}));const U=[...F];async function x(){if(u())try{const t=await r.plugins.adminConfig();t.routes?.length&&U.push(...t.routes);for(const[e,i]of Object.entries(t.views||{}))try{const o=await import(`/plugins/${i.entry}`);m[e]=B(o[i.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:i}]of Object.entries(t.takeovers||{}))i&&(b[`/${e}`]=i);for(const[e,{enabled:i,displayName:o}]of Object.entries(t.tools||{}))i===!1&&(y[`/${e}`]=o);for(const{id:e,href:i}of t.css||[])if(!document.getElementById(e)){const o=document.createElement("link");o.id=e,o.rel="stylesheet",o.href=i,document.head.appendChild(o)}}catch{}}function T(){const t=O();if(!t||$("#topbar-user-name").length)return;const i={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=ot(t);$("#topbar-user").html(`
|
|
2
|
-
${o?`<img id="topbar-avatar" class="topbar-avatar" src="${
|
|
3
|
-
<span id="topbar-user-name" class="topbar-user-name">${
|
|
4
|
-
<span class="topbar-role-badge topbar-role-badge--${
|
|
1
|
+
import{views as q}from"./views/index.js";import{withPluginChrome as B}from"./lib/plugin-chrome.js";import{api as r,getUser as F,isAuthenticated as c,logout as G,sendToLogin as z}from"./api.js";import{isAuthScreen as C,loginHashFor as K}from"./lib/login-arrange.js";import{installHttpInterceptor as J}from"./http-interceptor.js";import{installResizableSlideovers as Y}from"./lib/slideover-resizable.js";import{installActivityThrobber as Q}from"./lib/activity-throbber.js";import{isPathAllowed as X,redirectFor as Z}from"./lib/admin-scope.js";import{deliverDesktop as tt,desktopActive as et,notificationSettings as it}from"./lib/desktop-notify.js";import{avatarUrl as ot}from"./lib/avatar.js";import{installCalculator as at}from"./lib/calculator.js";$(()=>{J(),at(),Q(),Y(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");c()?await i(r,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let m=null,l="/",h=!1,w=null;function D(t){if(y[t])return E.toast(`${y[t]} is switched off on this site.`,{type:"info"}),"/";const i=b[t]||Z(m,t);return i||((t.split("?")[0]||"/")==="/"&&l&&l!=="/"?b[l]||l:null)}const b={},y={};R.use(async(t,i,e)=>{if(C(t.path))return e();if(!c()){z();return}const o=D(t.path);if(o){R.navigate(o);return}e()});async function g(){try{const t=await r.get("/auth/permissions");m=t.adminScope||null,l=t.adminHome||"/";const i=t.permissions||[];return h=X(m,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),w=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return m=null,l="/",h=!1,w=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{g()});let N=null,P=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(N){P=!0;return}N=(async()=>{do{P=!1;try{await j(await g())}catch{}}while(P)})().finally(()=>{N=null})});async function j(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:m})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(u&&(clearInterval(u),u=null),m=null,l="/",h=!1,w=null)});const O=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Plugin - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:i})=>{if(!C(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),C(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(r)),await I();const e=await g(),o=D(t.path);if(o&&R.navigate(o),j(e),it.read().signInToasts)try{const a=await r.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const f=d(s.data?.title||""),T=d((s.data?.body||"").slice(0,120));E.toast(`${f} - ${T}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}x()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const _=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function V(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const i=t?String(t.path).split("?")[0]:"";if(!t||!V(i))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(i))return;let a=0;const n=60,s=1500,f=()=>{a+=n;for(const T of _){const v=document.querySelector(T);if(!v)continue;if(Array.from(v.options||[]).find(H=>H.value===o)){v.value=o,v.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(f,n)};setTimeout(f,n)});const U="cms_card_states",A=S.get(U)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&A[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(A[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(U,A))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const p={...q},W={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},dashboard:{displayName:"Dashboard",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},blockEditor:{displayName:"Blocks",section:"Edit",icon:"box"},componentEditor:{displayName:"Components",section:"Edit",icon:"component"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,i]of Object.entries(W))p[t]&&(p[t]=B(p[t],{plugin:t,licence:"Built in",...i}));const k=[...O];async function I(){if(c())try{const t=await r.plugins.adminConfig(),i=new Set(k.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||k.push(e);for(const[e,o]of Object.entries(t.views||{}))try{const a=await import(`/plugins/${o.entry}`);p[e]=B(a[o.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(b[`/${e}`]=o);for(const[e,{enabled:o,displayName:a}]of Object.entries(t.tools||{}))o===!1&&(y[`/${e}`]=a);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const a=document.createElement("link");a.id=e,a.rel="stylesheet",a.href=o,document.head.appendChild(a)}}catch{}}function x(){const t=F();if(!t||$("#topbar-user-name").length)return;const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=ot(t);$("#topbar-user").html(`
|
|
2
|
+
${o?`<img id="topbar-avatar" class="topbar-avatar" src="${d(o)}" alt="">`:""}
|
|
3
|
+
<span id="topbar-user-name" class="topbar-user-name">${d(t.name)}</span>
|
|
4
|
+
<span class="topbar-role-badge topbar-role-badge--${d(t.role)}">${e}</span>
|
|
5
5
|
`),$("#topbar-actions").html(`
|
|
6
6
|
<a href="#/system/notifications" id="topbar-bell" class="topbar-action-link" data-tooltip="Notifications" data-tooltip-placement="bottom" hidden>
|
|
7
7
|
<span data-icon="bell"></span>
|
|
@@ -19,4 +19,4 @@ import{views as q}from"./views/index.js";import{withPluginChrome as B}from"./lib
|
|
|
19
19
|
<span data-icon="log-out"></span>
|
|
20
20
|
<span>Sign out</span>
|
|
21
21
|
</a>
|
|
22
|
-
`);const a=$("#topbar-bell").get(0);a&&(a.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!h),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),G()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),L(),
|
|
22
|
+
`);const a=$("#topbar-bell").get(0);a&&(a.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!h),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),G()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),L(),u||(u=setInterval(L,6e4))}function d(t){return String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}let u=null;async function L(){if(!(!c()||w===!1))try{const{count:t}=await r.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const i=$("#topbar-bell-badge");t>0?i.text(t>99?"99+":String(t)).removeAttr("hidden"):i.attr("hidden","").text(""),t>0&&et()&&tt(await r.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!c()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=K(window.location.hash));else if(await I(),F()){const a=await g(),n=(window.location.hash||"#/").slice(1)||"/",s=D(n);s&&(window.location.hash=`#${s}`),await j(a),x()}const t=R._extractParams.bind(R);R._extractParams=function(o,a){const n=a.indexOf("?");if(n!==-1&&(a=a.slice(0,n)),o.endsWith("/*")){const s=o.slice(0,-2);return a.startsWith(s+"/")?{}:null}return t(o,a)};const i=R._renderNotFound.bind(R);let e="";R._renderNotFound=async function(o){return c()&&e!==o&&(e=o,await I(),R._matchRoute(o).route)?(e="",R._handleRouteChange()):(e="",c()&&$("#admin-sidebar").get(0)?.style.display==="none"?(E.toast("That link goes nowhere - here is your home page.",{type:"warning"}),R.navigate("/")):i(o))},R.init({container:"#view-container",routes:k,views:p,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"note": "Written by `make sync-free-tier` in dcms-marketplace. Do not edit these plugins here - edit them there and sync.",
|
|
3
|
-
"source": "dcms-marketplace@
|
|
3
|
+
"source": "dcms-marketplace@ef8c865",
|
|
4
4
|
"plugins": {
|
|
5
5
|
"blog": {
|
|
6
6
|
"version": "1.9.4",
|
|
@@ -175,9 +175,9 @@
|
|
|
175
175
|
}
|
|
176
176
|
},
|
|
177
177
|
"shopping-cart": {
|
|
178
|
-
"version": "1.0
|
|
178
|
+
"version": "1.1.0",
|
|
179
179
|
"files": {
|
|
180
|
-
"CLAUDE.md": "
|
|
180
|
+
"CLAUDE.md": "70765cf061e97d0a57890d7731e0624b224e36267df42741e763e3efcd076d05",
|
|
181
181
|
"admin/css/index.css": "a6bab1d0072e364ee1d94c2d7d7b99c3660ba91c5eba537e269a579f5b17f924",
|
|
182
182
|
"admin/lib/kit.js": "c2111d1b10c6c8b18bc061f9f55c83ee8baecc2bcff66f74eb38298f39886d86",
|
|
183
183
|
"admin/templates/shop.html": "c5bb03f1f7cbabfbd8202f3533783eb1848103755b1ba25fdcf1fd8e9478cb8e",
|
|
@@ -193,18 +193,18 @@
|
|
|
193
193
|
"inject/body.html": "de20f92c83229545d20e5186fd37ef0046a8820881bff34e7df5391cd7a2c75d",
|
|
194
194
|
"lib/extensions.js": "2cba90efef5b437ace557c7265fe7363ac7923d155681087a63e31e1168f6601",
|
|
195
195
|
"lib/payments.js": "84ad14813688de1f38448b4935180a786cda8af2f7f4b8890a8f1d426982d1aa",
|
|
196
|
-
"lib/pricing.js": "
|
|
196
|
+
"lib/pricing.js": "3ae965e042f5af0acdcecc66d27145ca5f608c656717288832b881a2a181dd2e",
|
|
197
197
|
"lib/render.js": "310d528f69cd0160db3bd3d3c88f16221e43004c93856587175c0bb149fe3a3d",
|
|
198
198
|
"lib/samples.js": "912b3d5bd543dffa5f870caf3c2b873f642fd55e3ebf3f97e1b31eeaaac77d3b",
|
|
199
|
-
"lib/shop.js": "
|
|
199
|
+
"lib/shop.js": "4399931dd46ab4dc9bd28e8613c992420ab822bd694b1495ef2b4ad6483b5153",
|
|
200
200
|
"lib/summary.js": "5697ffa45c9cc5338bbcb6eada08671c3e8376c49c629d2ae99b022397523f71",
|
|
201
|
-
"plugin.js": "
|
|
202
|
-
"plugin.json": "
|
|
203
|
-
"plugin.public.js": "
|
|
201
|
+
"plugin.js": "1f8477188b4054d172b37d953a439b4d5d4a532c6d26daaf9808346f332ddfcf",
|
|
202
|
+
"plugin.json": "eccf7d9bf2f7fdde26d78c82b944897031387b42ab26daa04c59d7a5cac3ea5f",
|
|
203
|
+
"plugin.public.js": "98fa32b6c69db4a967577f3536fc521c926053319dd20d2bae8bc7e94830e3e8",
|
|
204
204
|
"public/cart.js": "fcc706773a36e4bfd02ea235bcdad629137e996218e0a0e23892e33c689f2a6e",
|
|
205
205
|
"public/lib/money.js": "d22b2b6f80dde6f7887b69e24188c05ea93c02217aba639b5331aa3bd616f4fc",
|
|
206
206
|
"public/lib/orders.js": "09478e20d238adc8e387b3d9efcb13c441898fab18387ce6a1a8a92304ac4c5b",
|
|
207
|
-
"public/lib/products.js": "
|
|
207
|
+
"public/lib/products.js": "65a033b4b03c1b6d9cd7035e6be4c3fe47c7ad309dc14c4d417b6ac0fc10e2f4",
|
|
208
208
|
"public/samples/candle-2.svg": "aca5691b9168467caf6feb7622ab154ee92504be8b887ac6c818db22476e7bed",
|
|
209
209
|
"public/samples/candle.svg": "ef75e95a53ce13e88928004c6f1290c45a1094765e2ba741ae1fc55d404b881f",
|
|
210
210
|
"public/samples/mug-2.svg": "5f37d23e90b7e79e42d38009a56b3ea26adae03586830a0cd418565491bd4532",
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
"public/samples/tote.svg": "01de817a32c8d3d8ea47ff491049f439ca3e4d4fce943dbdc62a4187139e016c",
|
|
220
220
|
"public/shop.css": "277904382e491f0ecc76aad916126970b87e161ef06ac6deeff193358b4e4c31",
|
|
221
221
|
"templates/shop.html": "4649e57d23458833a157799dc80141824eb0f658d96734bd1859c68e0ba9a8b9",
|
|
222
|
-
"tests/api.test.js": "
|
|
222
|
+
"tests/api.test.js": "04105c5d4f7bf4fd7b9e4dea5e8b8de4a4ebbba0a8795b47c166b0a6798fd089",
|
|
223
223
|
"tests/pure.test.js": "f00f27ee8c941d6268eb780520007b7413b0872e2590487f1b460ce5ea09dbb1"
|
|
224
224
|
}
|
|
225
225
|
}
|
|
@@ -97,3 +97,18 @@ A cached page can show a stale stock flag until it is re-rendered; checkout re-p
|
|
|
97
97
|
- A test site on a non-standard port needs `site.baseUrl` for Stripe/PayPal return addresses behind a proxy.
|
|
98
98
|
- Admin views: item templates live in JS (a templateUrl goes through _.render); checkboxes are set after render
|
|
99
99
|
and listened to; slideovers via kit.openPanel (disposed and removed on close).
|
|
100
|
+
|
|
101
|
+
## 1.1.0: extensions sell their own things
|
|
102
|
+
|
|
103
|
+
- **`services.startCheckout`** (`{lines, customer, provider, ref, returnTo, base}`) prices and orders exactly as the
|
|
104
|
+
storefront's checkout. Both go through `openPayment()`.
|
|
105
|
+
- The order keeps the extension's `ref`, which comes back in `on.paid(order)`, and its `returnTo` page.
|
|
106
|
+
- Stripe success, cancel and the PayPal return go to that page instead of the receipt.
|
|
107
|
+
- It only sells things that are not posted.
|
|
108
|
+
- **`services.checkOrder(id)`** asks Stripe about a pending order. An extension's page calls it on the return,
|
|
109
|
+
as the receipt does, so a missing webhook does not leave it pending.
|
|
110
|
+
- **Unlisted products** (`unlisted: true`, `owner: '<plugin>'`) are an extension's: never on the storefront, refused
|
|
111
|
+
by its checkout, and bought only through `startCheckout`.
|
|
112
|
+
- The product form can never set or clear the two fields (POST strips them, PUT keeps the stored ones).
|
|
113
|
+
- A copy is the shop's own.
|
|
114
|
+
- `services.saveProduct` and `services.removeProduct` are how the extension manages them.
|
|
@@ -121,7 +121,8 @@ export async function priceBasket({lines, products, settings, input = {}, handle
|
|
|
121
121
|
for (const l of lines) {
|
|
122
122
|
const entry = products.get(l.productId);
|
|
123
123
|
const p = entry?.data;
|
|
124
|
-
|
|
124
|
+
// An unlisted product (an extension's) is sold only through that extension's checkout.
|
|
125
|
+
if (!p || p.status !== 'active' || (p.unlisted && !input.allowUnlisted)) {
|
|
125
126
|
problems.push({productId: l.productId, variantId: l.variantId, message: 'No longer on sale - taken out.'});
|
|
126
127
|
continue;
|
|
127
128
|
}
|
|
@@ -230,7 +230,7 @@ async function nextNumber() {
|
|
|
230
230
|
*
|
|
231
231
|
* @returns {Promise<{id, data}>}
|
|
232
232
|
*/
|
|
233
|
-
export function createOrder({quote, checkout, provider, delivery, code = ''}) {
|
|
233
|
+
export function createOrder({quote, checkout, provider, delivery, code = '', ref = '', returnTo = ''}) {
|
|
234
234
|
return withLock(async () => {
|
|
235
235
|
const now = new Date().toISOString();
|
|
236
236
|
const data = {
|
|
@@ -257,6 +257,10 @@ export function createOrder({quote, checkout, provider, delivery, code = ''}) {
|
|
|
257
257
|
address: checkout.address,
|
|
258
258
|
note: checkout.note,
|
|
259
259
|
code,
|
|
260
|
+
// An extension's own reference (services.startCheckout) - it comes back in on.paid(order).
|
|
261
|
+
ref: String(ref || '').slice(0, 64),
|
|
262
|
+
// Where an extension's shopper goes back to after paying, instead of the Shop's receipt.
|
|
263
|
+
returnTo: String(returnTo || '').slice(0, 500),
|
|
260
264
|
provider,
|
|
261
265
|
providerRef: '',
|
|
262
266
|
paymentRef: '',
|
|
@@ -58,7 +58,7 @@ const receiptPath = (s, order) => `${s.shopPath}/order/${order.id}?t=${order.dat
|
|
|
58
58
|
const fail = (reply, status, error, extra = {}) => reply.code(status).send({error, ...extra});
|
|
59
59
|
|
|
60
60
|
/** The quote, from a request body, at today's prices. */
|
|
61
|
-
async function quoteFor(body, settings) {
|
|
61
|
+
async function quoteFor(body, settings, {allowUnlisted = false} = {}) {
|
|
62
62
|
const lines = cleanLines(body?.lines);
|
|
63
63
|
const ids = new Set(lines.map(l => l.productId));
|
|
64
64
|
const products = new Map();
|
|
@@ -70,7 +70,8 @@ async function quoteFor(body, settings) {
|
|
|
70
70
|
postcode: String(body?.address?.postcode || '').slice(0, 20),
|
|
71
71
|
delivery: body?.delivery === 'collect' ? 'collect' : 'ship',
|
|
72
72
|
service: String(body?.service || '').slice(0, 40),
|
|
73
|
-
email: String(body?.customer?.email || '').slice(0, 200)
|
|
73
|
+
email: String(body?.customer?.email || '').slice(0, 200),
|
|
74
|
+
allowUnlisted
|
|
74
75
|
};
|
|
75
76
|
const quote = await priceBasket({lines, products, settings, input, handlers: cartRegistry().handlers()});
|
|
76
77
|
return {quote, input};
|
|
@@ -95,7 +96,7 @@ function shopperQuote(q, settings) {
|
|
|
95
96
|
*/
|
|
96
97
|
function registerShortcodes(hooks) {
|
|
97
98
|
if (typeof hooks.registerShortcode !== 'function') return;
|
|
98
|
-
const onSale = () => shelfNow().filter(e => e.data?.status === 'active').sort(byShelf);
|
|
99
|
+
const onSale = () => shelfNow().filter(e => e.data?.status === 'active' && !e.data?.unlisted).sort(byShelf);
|
|
99
100
|
|
|
100
101
|
hooks.registerShortcode('shop-products', (attrStr, _body, ctx) => {
|
|
101
102
|
const a = ctx.parseShortcodeAttrs(attrStr || '');
|
|
@@ -210,14 +211,19 @@ export default async function shoppingCart(fastify, options) {
|
|
|
210
211
|
|
|
211
212
|
fastify.post('/products', can('manage'), async (request, reply) => {
|
|
212
213
|
const s = await liveSettings();
|
|
213
|
-
|
|
214
|
+
// unlisted and owner are an extension's to set (services.saveProduct), never the product form's.
|
|
215
|
+
const {unlisted, owner, ...body} = request.body || {};
|
|
216
|
+
const r = await saveProduct(body, {currency: s.currency, by: request.user?.id});
|
|
214
217
|
if (r.errors) return fail(reply, 400, Object.values(r.errors)[0], {errors: r.errors});
|
|
215
218
|
return reply.code(201).send(r.entry);
|
|
216
219
|
});
|
|
217
220
|
|
|
218
221
|
fastify.put('/products/:id', can('manage'), async (request, reply) => {
|
|
219
222
|
const s = await liveSettings();
|
|
220
|
-
|
|
223
|
+
// An edit in the product form keeps an extension's product unlisted and its own: the form never sends them.
|
|
224
|
+
const had = await getProduct(request.params.id);
|
|
225
|
+
const {unlisted, owner, ...body} = request.body || {};
|
|
226
|
+
const r = await saveProduct({...body, unlisted: had?.data?.unlisted === true, owner: had?.data?.owner || ''}, {id: request.params.id, currency: s.currency});
|
|
221
227
|
if (r.notFound) return fail(reply, 404, 'That product is gone.');
|
|
222
228
|
if (r.errors) return fail(reply, 400, Object.values(r.errors)[0], {errors: r.errors});
|
|
223
229
|
return r.entry;
|
|
@@ -240,7 +246,8 @@ export default async function shoppingCart(fastify, options) {
|
|
|
240
246
|
const had = await getProduct(request.params.id);
|
|
241
247
|
if (!had) return fail(reply, 404, 'That product is gone.');
|
|
242
248
|
const s = await liveSettings();
|
|
243
|
-
|
|
249
|
+
// A copy is the shop's own product, even of an extension's: listed, and nobody else's to change.
|
|
250
|
+
const r = await saveProduct({...had.data, name: `${had.data.name} (copy)`, slug: '', status: 'draft', unlisted: false, owner: '',
|
|
244
251
|
variants: (had.data.variants || []).map(v => ({...v, id: ''}))}, {currency: s.currency, by: request.user?.id});
|
|
245
252
|
if (r.errors) return fail(reply, 400, Object.values(r.errors)[0]);
|
|
246
253
|
return reply.code(201).send(r.entry);
|
|
@@ -390,7 +397,7 @@ export default async function shoppingCart(fastify, options) {
|
|
|
390
397
|
|
|
391
398
|
fastify.get('/catalogue', async (request) => {
|
|
392
399
|
const s = await liveSettings();
|
|
393
|
-
const products = (await allProducts()).filter(e => e.data?.status === 'active').sort(byShelf).map(publicProduct);
|
|
400
|
+
const products = (await allProducts()).filter(e => e.data?.status === 'active' && !e.data?.unlisted).sort(byShelf).map(publicProduct);
|
|
394
401
|
return {currency: s.currency, shopPath: s.shopPath, products};
|
|
395
402
|
});
|
|
396
403
|
|
|
@@ -424,46 +431,9 @@ export default async function shoppingCart(fastify, options) {
|
|
|
424
431
|
|
|
425
432
|
const order = await createOrder({quote, checkout, provider, delivery: collecting ? 'collect' : (quote.requiresShipping ? 'ship' : 'none'), code: input.code});
|
|
426
433
|
registry.emit('checkout', order);
|
|
427
|
-
const base = siteBase(request);
|
|
428
|
-
const receipt = `${base}${receiptPath(s, order)}`;
|
|
429
|
-
|
|
430
434
|
try {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
return {redirect: receipt, orderId: order.id};
|
|
434
|
-
}
|
|
435
|
-
if (provider === 'offline') {
|
|
436
|
-
const {order: moved} = await moveOrder(order.id, 'awaiting', {by: 'shopper', note: s.offlineLabel});
|
|
437
|
-
registry.emit('status', moved, 'pending', 'awaiting');
|
|
438
|
-
try {
|
|
439
|
-
hooks.notify?.({title: `Order ${orderLabel(moved.data)} awaiting payment - ${formatMoney(moved.data.total, moved.data.currency)}`,
|
|
440
|
-
body: `${moved.data.customer?.name}: ${s.offlineLabel}`, severity: 'info', source: 'orders',
|
|
441
|
-
link: `#/plugins/shopping-cart/orders/${order.id}`, dedupeKey: `order-awaiting:${order.id}`});
|
|
442
|
-
} catch { /* never mind */ }
|
|
443
|
-
return {redirect: receipt, orderId: order.id};
|
|
444
|
-
}
|
|
445
|
-
if (provider === 'stripe') {
|
|
446
|
-
const session = await pay.stripeCreateSession({
|
|
447
|
-
secretKey: s.stripeSecretKey,
|
|
448
|
-
order: {id: order.id, ...order.data},
|
|
449
|
-
successUrl: `${receipt}&session_id={CHECKOUT_SESSION_ID}`,
|
|
450
|
-
cancelUrl: `${base}${s.shopPath}/cart?cancelled=${order.id}`,
|
|
451
|
-
shopName: s.shopTitle
|
|
452
|
-
});
|
|
453
|
-
await setProviderRef(order.id, session.id);
|
|
454
|
-
return {redirect: session.url, orderId: order.id};
|
|
455
|
-
}
|
|
456
|
-
if (provider === 'paypal') {
|
|
457
|
-
const pp = await pay.paypalCreateOrder({
|
|
458
|
-
creds: {clientId: s.paypalClientId, secret: s.paypalSecret, mode: s.paypalMode},
|
|
459
|
-
order: {id: order.id, ...order.data},
|
|
460
|
-
returnUrl: `${base}/api/plugins/shopping-cart/paypal/return?order=${order.id}&t=${order.data.token}`,
|
|
461
|
-
cancelUrl: `${base}${s.shopPath}/cart?cancelled=${order.id}`,
|
|
462
|
-
shopName: s.shopTitle
|
|
463
|
-
});
|
|
464
|
-
await setProviderRef(order.id, pp.id);
|
|
465
|
-
return {redirect: pp.url, orderId: order.id};
|
|
466
|
-
}
|
|
435
|
+
const opened = await openPayment(order, s, provider, siteBase(request));
|
|
436
|
+
if (opened) return opened;
|
|
467
437
|
} catch (err) {
|
|
468
438
|
request.log.warn(`[shopping-cart] checkout ${order.data.number}: ${err.message}`);
|
|
469
439
|
return fail(reply, 502, `The payment page could not be opened: ${err.message.replace(/^(Stripe|PayPal): /, '')}. Nothing has been charged.`);
|
|
@@ -471,6 +441,59 @@ export default async function shoppingCart(fastify, options) {
|
|
|
471
441
|
return fail(reply, 400, 'Choose how to pay.');
|
|
472
442
|
});
|
|
473
443
|
|
|
444
|
+
/** Where a shopper goes after paying: an extension's own page when it asked for one, else the receipt. */
|
|
445
|
+
const afterPay = (base, s, order, extra = '') => {
|
|
446
|
+
const to = order.data.returnTo;
|
|
447
|
+
if (to) return `${to}${to.includes('?') ? '&' : '?'}order=${order.id}${extra ? `&${extra}` : ''}`;
|
|
448
|
+
return `${base}${receiptPath(s, order)}${extra ? `&${extra}` : ''}`;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Open the way to pay for an order: the provider's page, or straight to the receipt (free, offline).
|
|
453
|
+
* The storefront and services.startCheckout both come through here. Null when the method is unknown.
|
|
454
|
+
*/
|
|
455
|
+
async function openPayment(order, s, provider, base) {
|
|
456
|
+
const receipt = afterPay(base, s, order);
|
|
457
|
+
const cancel = order.data.returnTo ? afterPay(base, s, order, 'cancelled=1') : `${base}${s.shopPath}/cart?cancelled=${order.id}`;
|
|
458
|
+
if (provider === 'free') {
|
|
459
|
+
await markPaid(order.id, {by: 'free', note: 'Nothing to pay'}).then(r => r.changed && announcePaid(r.order));
|
|
460
|
+
return {redirect: receipt, orderId: order.id};
|
|
461
|
+
}
|
|
462
|
+
if (provider === 'offline') {
|
|
463
|
+
const {order: moved} = await moveOrder(order.id, 'awaiting', {by: 'shopper', note: s.offlineLabel});
|
|
464
|
+
registry.emit('status', moved, 'pending', 'awaiting');
|
|
465
|
+
try {
|
|
466
|
+
hooks.notify?.({title: `Order ${orderLabel(moved.data)} awaiting payment - ${formatMoney(moved.data.total, moved.data.currency)}`,
|
|
467
|
+
body: `${moved.data.customer?.name}: ${s.offlineLabel}`, severity: 'info', source: 'orders',
|
|
468
|
+
link: `#/plugins/shopping-cart/orders/${order.id}`, dedupeKey: `order-awaiting:${order.id}`});
|
|
469
|
+
} catch { /* never mind */ }
|
|
470
|
+
return {redirect: receipt, orderId: order.id};
|
|
471
|
+
}
|
|
472
|
+
if (provider === 'stripe') {
|
|
473
|
+
const session = await pay.stripeCreateSession({
|
|
474
|
+
secretKey: s.stripeSecretKey,
|
|
475
|
+
order: {id: order.id, ...order.data},
|
|
476
|
+
successUrl: `${receipt}&session_id={CHECKOUT_SESSION_ID}`,
|
|
477
|
+
cancelUrl: cancel,
|
|
478
|
+
shopName: s.shopTitle
|
|
479
|
+
});
|
|
480
|
+
await setProviderRef(order.id, session.id);
|
|
481
|
+
return {redirect: session.url, orderId: order.id};
|
|
482
|
+
}
|
|
483
|
+
if (provider === 'paypal') {
|
|
484
|
+
const pp = await pay.paypalCreateOrder({
|
|
485
|
+
creds: {clientId: s.paypalClientId, secret: s.paypalSecret, mode: s.paypalMode},
|
|
486
|
+
order: {id: order.id, ...order.data},
|
|
487
|
+
returnUrl: `${base}/api/plugins/shopping-cart/paypal/return?order=${order.id}&t=${order.data.token}`,
|
|
488
|
+
cancelUrl: cancel,
|
|
489
|
+
shopName: s.shopTitle
|
|
490
|
+
});
|
|
491
|
+
await setProviderRef(order.id, pp.id);
|
|
492
|
+
return {redirect: pp.url, orderId: order.id};
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
474
497
|
/**
|
|
475
498
|
* An order's receipt, for the page and for its polling. A pending Stripe
|
|
476
499
|
* order is checked with Stripe on the way: the shopper's return is the
|
|
@@ -503,12 +526,59 @@ export default async function shoppingCart(fastify, options) {
|
|
|
503
526
|
}
|
|
504
527
|
registry.services.confirmStripe = confirmStripe;
|
|
505
528
|
|
|
529
|
+
/**
|
|
530
|
+
* A checkout started by an extension (Cards Pro selling credits) for things that are not posted:
|
|
531
|
+
* priced, ordered and paid exactly as the storefront's, with the extension's own `ref` on the order
|
|
532
|
+
* (it comes back in on.paid) and its own page to come back to. Throws {status, message}.
|
|
533
|
+
*
|
|
534
|
+
* @param {object} p
|
|
535
|
+
* @param {{productId: string, qty: number}[]} p.lines
|
|
536
|
+
* @param {{name: string, email: string}} p.customer
|
|
537
|
+
* @param {string} [p.provider] - one of the Shop's methods; the first when left out
|
|
538
|
+
* @param {string} [p.ref] - the extension's reference, 64 characters at most
|
|
539
|
+
* @param {string} [p.returnTo] - a path on this site ('/admin/#/...'), where the shopper comes back to
|
|
540
|
+
* @param {string} [p.base] - this site's origin, from the extension's request
|
|
541
|
+
*/
|
|
542
|
+
async function startCheckout({lines, customer, provider = '', ref = '', returnTo = '', base = ''} = {}) {
|
|
543
|
+
const bad = (status, message) => Object.assign(new Error(message), {status});
|
|
544
|
+
const s = await liveSettings();
|
|
545
|
+
const {quote} = await quoteFor({lines, customer}, s, {allowUnlisted: true});
|
|
546
|
+
if (!quote.lines.length) throw bad(400, 'Nothing to buy.');
|
|
547
|
+
if (quote.problems.length) throw bad(409, quote.problems.map(p => p.message).join(' '));
|
|
548
|
+
if (quote.requiresShipping) throw bad(400, 'This checkout is only for things that are not posted.');
|
|
549
|
+
const checkout = cleanCheckout({customer}, {needsAddress: false});
|
|
550
|
+
if (Object.keys(checkout.errors).length) throw bad(400, Object.values(checkout.errors)[0]);
|
|
551
|
+
const methods = methodsOf(s).map(m => m.id);
|
|
552
|
+
const via = quote.total === 0 ? 'free' : (provider || methods[0] || '');
|
|
553
|
+
if (via !== 'free' && !methods.includes(via)) throw bad(409, 'The Shop has no way to take payment set up yet.');
|
|
554
|
+
const origin = /^https?:\/\/[^/\s]+$/i.test(base) ? base.replace(/\/+$/, '') : siteBase(null);
|
|
555
|
+
const back = /^\/(?!\/)/.test(String(returnTo)) ? `${origin}${returnTo}` : '';
|
|
556
|
+
const order = await createOrder({quote, checkout, provider: via, delivery: 'none', ref, returnTo: back});
|
|
557
|
+
registry.emit('checkout', order);
|
|
558
|
+
const opened = await openPayment(order, s, via, origin);
|
|
559
|
+
if (!opened) throw bad(400, 'Choose how to pay.');
|
|
560
|
+
return {...opened, provider: via, total: quote.total, currency: quote.currency};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** An order, with a pending Stripe payment asked about on the way (the shopper's return is how we usually learn). */
|
|
564
|
+
async function checkOrder(id) {
|
|
565
|
+
const o = await getOrder(String(id || ''));
|
|
566
|
+
return o ? confirmStripe(o, '', fastify.log) : null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
registry.services.startCheckout = startCheckout;
|
|
570
|
+
registry.services.checkOrder = checkOrder;
|
|
571
|
+
registry.services.paymentMethods = async () => methodsOf(await liveSettings()).map(m => ({id: m.id, label: m.label}));
|
|
572
|
+
// An extension looks after its own (unlisted) products: made, changed and removed through these.
|
|
573
|
+
registry.services.saveProduct = async (input, {id = null} = {}) => saveProduct(input, {id, currency: (await liveSettings()).currency});
|
|
574
|
+
registry.services.removeProduct = removeProduct;
|
|
575
|
+
|
|
506
576
|
fastify.get('/paypal/return', async (request, reply) => {
|
|
507
577
|
const {order: id, t, token} = request.query || {};
|
|
508
578
|
const o = await getOrder(String(id || ''));
|
|
509
579
|
const s = await liveSettings();
|
|
510
580
|
if (!o || !tokenOk(o, t)) return reply.redirect(`${s.shopPath}/cart`);
|
|
511
|
-
const receipt =
|
|
581
|
+
const receipt = afterPay('', s, o);
|
|
512
582
|
if (o.data.status !== 'pending' || o.data.provider !== 'paypal') return reply.redirect(receipt);
|
|
513
583
|
if (token !== o.data.providerRef) return reply.redirect(`${receipt}&problem=paypal`);
|
|
514
584
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shopping-cart",
|
|
3
3
|
"displayName": "Shopping Cart",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Sell from your site: products with options, stock and pictures, a storefront and a cart that follows shoppers from page to page, and checkout with Stripe, PayPal or payment offline. Orders to fulfil in the sidebar, refunds from the order.",
|
|
7
7
|
"author": "Domma CMS",
|
|
@@ -60,7 +60,7 @@ export default async function shopPublic(fastify) {
|
|
|
60
60
|
|
|
61
61
|
const pages = {
|
|
62
62
|
async index(request, reply, s) {
|
|
63
|
-
const entries = (await allProducts()).filter(e => e.data?.status === 'active').sort(byShelf);
|
|
63
|
+
const entries = (await allProducts()).filter(e => e.data?.status === 'active' && !e.data?.unlisted).sort(byShelf);
|
|
64
64
|
const category = String(request.query?.category || '').slice(0, 60);
|
|
65
65
|
const query = String(request.query?.q || '').slice(0, 80);
|
|
66
66
|
return send(reply, request, s, {
|
|
@@ -79,7 +79,7 @@ export default async function shopPublic(fastify) {
|
|
|
79
79
|
},
|
|
80
80
|
|
|
81
81
|
async product(request, reply, s, {slug}) {
|
|
82
|
-
const all = (await allProducts()).filter(e => e.data?.status === 'active');
|
|
82
|
+
const all = (await allProducts()).filter(e => e.data?.status === 'active' && !e.data?.unlisted);
|
|
83
83
|
const entry = all.find(e => e.data.slug === slug);
|
|
84
84
|
if (!entry) return null; // fall through to the CMS's own 404 (or a page at that address)
|
|
85
85
|
let descriptionHtml = '';
|
|
@@ -142,7 +142,12 @@ export function cleanProduct(input = {}, {currency = 'GBP', minor = true} = {})
|
|
|
142
142
|
variants,
|
|
143
143
|
shippable: input.shippable === undefined ? true : Boolean(input.shippable),
|
|
144
144
|
weight: Number.isInteger(weight) ? weight : null,
|
|
145
|
-
taxClass: TAX_CLASSES.includes(input.taxClass) ? input.taxClass : 'standard'
|
|
145
|
+
taxClass: TAX_CLASSES.includes(input.taxClass) ? input.taxClass : 'standard',
|
|
146
|
+
// Sold only through an extension's own checkout (Cards Pro's credit packs): never on the
|
|
147
|
+
// storefront, where we would know nothing of who bought it but an email address.
|
|
148
|
+
unlisted: input.unlisted === true,
|
|
149
|
+
// The plugin that looks after it (it may change or remove it); '' for the shop's own products.
|
|
150
|
+
owner: /^[a-z0-9-]{0,60}$/.test(String(input.owner ?? '')) ? String(input.owner ?? '') : ''
|
|
146
151
|
};
|
|
147
152
|
return {data, errors};
|
|
148
153
|
}
|
|
@@ -146,3 +146,45 @@ test('the Stripe webhook refuses an unsigned body', async () => {
|
|
|
146
146
|
headers: {'content-type': 'application/json', 'stripe-signature': 't=1,v1=00'}});
|
|
147
147
|
assert.equal(res.statusCode, 400);
|
|
148
148
|
});
|
|
149
|
+
|
|
150
|
+
test('an extension sells its own unlisted product through services.startCheckout, and gets its ref back when paid', async () => {
|
|
151
|
+
const {cartRegistry} = await import('../lib/extensions.js');
|
|
152
|
+
const reg = cartRegistry();
|
|
153
|
+
const paid = [];
|
|
154
|
+
reg.register('test-credits', {on: {paid: (order) => paid.push(order)}});
|
|
155
|
+
try {
|
|
156
|
+
await call('PUT', '/settings', {offlineEnabled: true, countries: ['GB']});
|
|
157
|
+
const made = await reg.services.saveProduct({name: '5 card credits', price: 100, status: 'active', shippable: false, unlisted: true, owner: 'test-credits'});
|
|
158
|
+
const pack = made.entry;
|
|
159
|
+
assert.equal(pack.data.unlisted, true);
|
|
160
|
+
// Not on the storefront, and not buyable there.
|
|
161
|
+
assert.ok(!(await call('GET', '/catalogue')).body.products.some(p => p.name === '5 card credits'));
|
|
162
|
+
const q = await call('POST', '/quote', {lines: [{productId: pack.id, qty: 1}]});
|
|
163
|
+
assert.equal(q.body.lines.length, 0, 'the storefront checkout refuses it');
|
|
164
|
+
// The product form cannot list it by accident.
|
|
165
|
+
const edited = await call('PUT', `/products/${pack.id}`, {...pack.data, name: '5 credits', unlisted: false, owner: ''});
|
|
166
|
+
assert.equal(edited.body.data.unlisted, true);
|
|
167
|
+
assert.equal(edited.body.data.owner, 'test-credits');
|
|
168
|
+
// The extension's checkout: priced as the shop prices, the ref kept, back to its own page.
|
|
169
|
+
const out = await reg.services.startCheckout({lines: [{productId: pack.id, qty: 1}], customer: {name: 'Pat', email: 'pat@x.test'},
|
|
170
|
+
provider: 'offline', ref: 'purchase-123', returnTo: '/admin/#/plugins/test-credits?purchase=123', base: 'https://site.test'});
|
|
171
|
+
assert.equal(out.total, 100);
|
|
172
|
+
assert.match(out.redirect, /^https:\/\/site\.test\/admin\/#\/plugins\/test-credits\?purchase=123&order=/);
|
|
173
|
+
const order = await reg.services.checkOrder(out.orderId);
|
|
174
|
+
assert.equal(order.data.ref, 'purchase-123');
|
|
175
|
+
await assert.rejects(reg.services.startCheckout({lines: [{productId: pack.id, qty: 1}], customer: {name: '', email: 'x'}, provider: 'offline'}), /name/i);
|
|
176
|
+
await assert.rejects(reg.services.startCheckout({lines: [{productId: pack.id, qty: 1}], customer: {name: 'Pat', email: 'pat@x.test'}, provider: 'bitcoin'}), /payment/i);
|
|
177
|
+
// Paid (here by hand, as an offline order is): the extension hears, with its ref.
|
|
178
|
+
const r = await call('PUT', `/orders/${out.orderId}/status`, {status: 'paid'});
|
|
179
|
+
assert.ok([200, 201].includes(r.status), JSON.stringify(r.body));
|
|
180
|
+
await new Promise(res => setTimeout(res, 20));
|
|
181
|
+
assert.equal(paid.at(-1)?.data?.ref, 'purchase-123');
|
|
182
|
+
// A free one is paid at once.
|
|
183
|
+
const freebie = (await reg.services.saveProduct({name: 'Free credit', price: 0, status: 'active', shippable: false, unlisted: true, owner: 'test-credits'})).entry;
|
|
184
|
+
const f = await reg.services.startCheckout({lines: [{productId: freebie.id, qty: 1}], customer: {name: 'Pat', email: 'pat@x.test'}, ref: 'gift-1', base: 'https://site.test'});
|
|
185
|
+
assert.equal(f.provider, 'free');
|
|
186
|
+
assert.equal((await reg.services.checkOrder(f.orderId)).data.status, 'paid');
|
|
187
|
+
} finally {
|
|
188
|
+
reg.unregister('test-credits');
|
|
189
|
+
}
|
|
190
|
+
});
|