@canopypay/checkout-sdk 0.3.0 → 0.4.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.
@@ -1 +1 @@
1
- "use strict";(()=>{(function(){"use strict";if(typeof window=="undefined")return;let f="width=460,height=640",y=2500,m="Pay with Canopy",h=500,g=420,v="canopy",b="cnpy_pk_live_";function k(){let e=document.currentScript;if(e&&e.src)try{return new URL(e.src).origin}catch(o){}let n=document.getElementsByTagName("script");for(let o=0;o<n.length;o+=1){let r=n[o].src;if(r&&/\/v1\/(?:[^/]+\/)?canopy\.js(?:[?#].*)?$/.test(r))try{return new URL(r).origin}catch(t){}}return null}function _(e){let n;try{n=new URL(e)}catch(t){return{ok:!1,code:"canopy_origin_invalid"}}if(!((n.pathname===""||n.pathname==="/")&&n.search===""&&n.hash===""))return{ok:!1,code:"canopy_origin_not_bare"};let r=n.hostname==="localhost"||n.hostname==="127.0.0.1";return n.protocol!=="https:"&&!r?{ok:!1,code:"canopy_origin_insecure"}:{ok:!0,origin:n.origin}}function E(e){if(e!==void 0)return _(e);let n=k();return n?{ok:!0,origin:n}:{ok:!1,code:"canopy_origin_undiscoverable"}}function c(e){let n=new URL("/customer-pay",e.origin);return n.searchParams.set("merchant",e.merchant),n.hash="cap="+encodeURIComponent(e.capability||""),n.toString()}function i(e,n,o){if(!e.destroyed)try{e.onEvent(Object.assign({type:n},o))}catch(r){}}function l(e){e.handshakeTimer&&(clearTimeout(e.handshakeTimer),e.handshakeTimer=null),e.resizeObserver&&(e.resizeObserver.disconnect(),e.resizeObserver=null),e.frame&&(e.frame.parentNode&&e.frame.parentNode.removeChild(e.frame),e.frame=null),e.lastHeight=null}function T(e){e.popupPollTimer&&clearInterval(e.popupPollTimer),e.popupPollTimer=setInterval(function(){try{e.popupWin&&e.popupWin.closed&&(e.popupPollTimer&&clearInterval(e.popupPollTimer),e.popupPollTimer=null,e.popupWin=null,i(e,"close",{via:"popup"}))}catch(n){e.popupPollTimer&&clearInterval(e.popupPollTimer),e.popupPollTimer=null}},h)}function p(e,n){if(!e.destroyed){if(!n){i(e,"error",{code:"popup_blocked"});return}if(!e.capability){try{n.close()}catch(o){}i(e,"error",{code:"capability_unavailable"});return}try{n.location.href=c(e)}catch(o){i(e,"error",{code:"popup_navigation_failed"});return}e.popupWin=n,T(e)}}function w(e){let n=document.createElement("button");n.type="button",n.textContent=e.fallbackLabel;let o=n.style;o.background="#1A7A4C",o.color="#FFFFFF",o.border="none",o.borderRadius="8px",o.height="48px",o.padding="0 20px",o.fontSize="16px",o.fontFamily="inherit",o.cursor="pointer",o.maxWidth="100%",o.overflow="hidden",o.textOverflow="ellipsis",o.whiteSpace="nowrap",o.display="inline-block",n.addEventListener("click",function(){let r=window.open("","canopy_checkout",f);p(e,r)}),e.node.appendChild(n),e.fallbackButton=n,i(e,"fallback_offered",{surface:"popup",reason:"iframe_unavailable"})}function O(e){let n=document.createElement("iframe");n.src=c(e),n.style.width="100%",n.style.border="none",n.setAttribute("allow","clipboard-write"),n.setAttribute("title","Canopy checkout"),n.style.height=g+"px",e.node.appendChild(n),e.frame=n,typeof ResizeObserver!="undefined"&&(e.resizeObserver=new ResizeObserver(function(o){for(let r=0;r<o.length;r+=1){let t=Math.round(o[r].contentRect.height);t>0&&t!==e.lastHeight&&(e.lastHeight=t,i(e,"resize",{height:t}))}}),e.resizeObserver.observe(n)),e.handshakeTimer=setTimeout(function(){e.handshakeTimer=null,i(e,"surface_unavailable",{attempted:"iframe"}),l(e),e.surface==="auto"?w(e):i(e,"error",{code:"iframe_blocked"})},e.handshakeTimeoutMs)}function L(e,n){if(n.origin!==e.origin)return;let o=n.data;if(!(!o||o.source!==v)){if(o.type==="ready"){e.handshakeTimer&&(clearTimeout(e.handshakeTimer),e.handshakeTimer=null),i(e,"ready");return}if(o.type==="resize"&&typeof o.height=="number"&&e.frame){e.frame.style.height=o.height+"px";return}if(o.type==="paid"){i(e,"paid",o);return}if(o.type==="close"){i(e,"close");return}}}let s=new WeakMap;function R(e){if(typeof e.merchant!="string"||e.merchant.indexOf(b)!==0){try{(e.onEvent||function(){})({type:"error",code:"invalid_merchant_key"})}catch(a){}return{openNow:function(){},destroy:function(){}}}let n=E(e.canopyOrigin);if(!n.ok){try{(e.onEvent||function(){})({type:"error",code:n.code})}catch(a){}return{openNow:function(){},destroy:function(){}}}let o=typeof e.target=="string"?document.querySelector(e.target):e.target;if(!o)return{openNow:function(){},destroy:function(){}};let r=s.get(o);if(r)return r;let t={node:o,capability:e.capability,merchant:e.merchant,origin:n.origin,surface:e.surface||"popup",handshakeTimeoutMs:e.handshakeTimeoutMs||y,fallbackLabel:e.fallbackLabel||m,onEvent:e.onEvent||function(){},destroyed:!1,messageListener:function(){},handshakeTimer:null,resizeObserver:null,frame:null,fallbackButton:null,lastHeight:null,popupWin:null,popupPollTimer:null};t.messageListener=function(a){L(t,a)},window.addEventListener("message",t.messageListener),(t.surface==="iframe"||t.surface==="auto")&&O(t);let d={openNow:function(a){p(t,a)},destroy:function(){t.destroyed||(t.destroyed=!0,window.removeEventListener("message",t.messageListener),l(t),t.popupPollTimer&&(clearInterval(t.popupPollTimer),t.popupPollTimer=null),t.fallbackButton&&(t.fallbackButton.parentNode&&t.fallbackButton.parentNode.removeChild(t.fallbackButton),t.fallbackButton=null),s.delete(o))}};return s.set(o,d),d}let u=window;u.Canopy&&u.Canopy.__version==="3"||(u.Canopy={mount:R,__version:"3"})})();})();
1
+ "use strict";(()=>{var S="theme";var T=["accent","background","surface","text","muted","border","radius","font"],B=new Map([["accent",{kind:"colour",targets:["--leaf","--primary","--accent","--ring"]}],["background",{kind:"colour",targets:["--paper","--background","background"]}],["surface",{kind:"colour",targets:["--surface","--card","--popover","--secondary"]}],["text",{kind:"colour",targets:["--ink","--foreground","--card-foreground","--popover-foreground","--secondary-foreground"]}],["muted",{kind:"colour",targets:["--ink-muted","--muted-foreground"]}],["border",{kind:"colour",targets:["--line","--border","--input"]}],["radius",{kind:"length",targets:["--radius"]}],["font",{kind:"font",targets:["--font-sans"]}]]),Y=/^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/,q=/^(rgb|rgba|hsl|hsla)\(([0-9a-z.,%/ +-]*)\)$/,Z=/^[+-]?(?:[0-9]{1,4}(?:\.[0-9]{1,4})?|\.[0-9]{1,4})$/,J=/^[+-]?(?:[0-9]{1,3}(?:\.[0-9]{1,4})?|\.[0-9]{1,4})%$/,Q=/^(?:0|(?:[0-9]{1,4}(?:\.[0-9]{1,4})?|\.[0-9]{1,4})(?:px|rem|em|%))$/,ee=/^[A-Za-z0-9 ,_-]+$/,ne=new Map([["transparent",{r:0,g:0,b:0,a:0}],["black",{r:0,g:0,b:0,a:1}],["white",{r:255,g:255,b:255,a:1}],["silver",{r:192,g:192,b:192,a:1}],["gray",{r:128,g:128,b:128,a:1}],["grey",{r:128,g:128,b:128,a:1}],["maroon",{r:128,g:0,b:0,a:1}],["red",{r:255,g:0,b:0,a:1}],["purple",{r:128,g:0,b:128,a:1}],["fuchsia",{r:255,g:0,b:255,a:1}],["magenta",{r:255,g:0,b:255,a:1}],["green",{r:0,g:128,b:0,a:1}],["lime",{r:0,g:255,b:0,a:1}],["olive",{r:128,g:128,b:0,a:1}],["yellow",{r:255,g:255,b:0,a:1}],["navy",{r:0,g:0,b:128,a:1}],["blue",{r:0,g:0,b:255,a:1}],["teal",{r:0,g:128,b:128,a:1}],["aqua",{r:0,g:255,b:255,a:1}],["cyan",{r:0,g:255,b:255,a:1}],["orange",{r:255,g:165,b:0,a:1}],["pink",{r:255,g:192,b:203,a:1}],["brown",{r:165,g:42,b:42,a:1}],["beige",{r:245,g:245,b:220,a:1}],["ivory",{r:255,g:255,b:240,a:1}],["indigo",{r:75,g:0,b:130,a:1}],["violet",{r:238,g:130,b:238,a:1}],["gold",{r:255,g:215,b:0,a:1}],["crimson",{r:220,g:20,b:60,a:1}],["salmon",{r:250,g:128,b:114,a:1}],["khaki",{r:240,g:230,b:140,a:1}],["lavender",{r:230,g:230,b:250,a:1}],["turquoise",{r:64,g:224,b:208,a:1}],["tan",{r:210,g:180,b:140,a:1}],["plum",{r:221,g:160,b:221,a:1}],["orchid",{r:218,g:112,b:214,a:1}],["coral",{r:255,g:127,b:80,a:1}],["tomato",{r:255,g:99,b:71,a:1}],["chocolate",{r:210,g:105,b:30,a:1}],["snow",{r:255,g:250,b:250,a:1}],["linen",{r:250,g:240,b:230,a:1}],["azure",{r:240,g:255,b:255,a:1}],["wheat",{r:245,g:222,b:179,a:1}],["thistle",{r:216,g:191,b:216,a:1}]]),te=new Set(["transparent","currentcolor","inherit","initial","unset","revert","revert-layer"]),re=new Set(["text","muted","accent"]);function y(t,n,o){return t<n?n:t>o?o:t}function x(t){if(!Z.test(t))return null;let n=Number(t);return Number.isFinite(n)?n:null}function v(t){if(!J.test(t))return null;let n=Number(t.slice(0,t.length-1));return Number.isFinite(n)?n/100:null}function A(t){let n=v(t);if(n!==null)return y(n,0,1)*255;let o=x(t);return o===null?null:y(o,0,255)}function oe(t){let n=v(t);if(n!==null)return y(n,0,1);let o=x(t);return o===null?null:y(o,0,1)}function ae(t){let n=t,o=1;n.endsWith("deg")?n=n.slice(0,n.length-3):n.endsWith("grad")?(n=n.slice(0,n.length-4),o=360/400):n.endsWith("turn")?(n=n.slice(0,n.length-4),o=360):n.endsWith("rad")&&(n=n.slice(0,n.length-3),o=180/Math.PI);let r=x(n);return r===null?null:(r*o%360+360)%360}function C(t,n,o){let r=o;return r<0&&(r+=1),r>1&&(r-=1),r<1/6?t+(n-t)*6*r:r<1/2?n:r<2/3?t+(n-t)*(2/3-r)*6:t}function ie(t,n,o,r){if(n===0){let l=o*255;return{r:l,g:l,b:l,a:r}}let u=o<.5?o*(1+n):o+n-o*n,d=2*o-u,s=t/360;return{r:C(d,u,s+1/3)*255,g:C(d,u,s)*255,b:C(d,u,s-1/3)*255,a:r}}function le(t){let n=t.trim();if(n==="")return null;if(n.indexOf(",")>=0){if(n.indexOf("/")>=0)return null;let d=n.split(",").map(function(s){return s.trim()});return d.length!==3&&d.length!==4?null:d}let o=n.split("/");if(o.length>2)return null;let r=o[0].trim().split(/ +/);if(r.length!==3)return null;if(o.length===1)return r;let u=o[1].trim();return u===""||u.indexOf(" ")>=0?null:r.concat([u])}function se(t){let n=q.exec(t);if(n===null)return null;let o=n[1],r=le(n[2]);if(r===null)return null;let u=1;if(r.length===4){let c=oe(r[3]);if(c===null)return null;u=c}if(o==="rgb"||o==="rgba"){let c=A(r[0]),f=A(r[1]),h=A(r[2]);return c===null||f===null||h===null?null:{r:c,g:f,b:h,a:u}}let d=ae(r[0]),s=v(r[1]),l=v(r[2]);return d===null||s===null||l===null?null:ie(d,y(s,0,1),y(l,0,1),u)}function ue(t){if(!Y.test(t))return null;let n=t.slice(1),o=n.length===3||n.length===4,r=o?1:2,u=function(s){let l=n.slice(s*r,s*r+r);return parseInt(o?l+l:l,16)},d=n.length===4||n.length===8;return{r:u(0),g:u(1),b:u(2),a:d?u(3)/255:1}}function R(t){let n=ue(t);if(n!==null)return n;let o=se(t);if(o!==null)return o;let r=ne.get(t.toLowerCase());return r===void 0?null:r}function O(t){let n=t/255;return n<=.03928?n/12.92:Math.pow((n+.055)/1.055,2.4)}function F(t){return .2126*O(t.r)+.7152*O(t.g)+.0722*O(t.b)}function L(t,n){let o=F(t),r=F(n),u=o>r?o:r,d=o>r?r:o;return(u+.05)/(d+.05)}function w(t,n){return t.a>=1?t:{r:t.r*t.a+n.r*(1-t.a),g:t.g*t.a+n.g*(1-t.a),b:t.b*t.a+n.b*(1-t.a),a:1}}var N=3,ce={light:[{r:243,g:250,b:246,a:1},{r:255,g:255,b:255,a:1}],dark:[{r:10,g:22,b:40,a:1},{r:26,g:40,b:60,a:1}]},H={r:255,g:255,b:255,a:1};function de(t,n){let o=n===null?["light","dark"]:[n],r=[];for(let l=0;l<o.length;l+=1){let c=ce[o[l]];for(let f=0;f<c.length;f+=1)r.push(c[f])}let u=[],d=["surface","background"];for(let l=0;l<d.length;l+=1){let c=t[d[l]];if(c===void 0)continue;let f=R(c);f!==null&&u.push(f)}if(u.length===0)return r;let s=[];for(let l=0;l<u.length;l+=1){let c=u[l];if(c.a>=1){s.push(c);continue}for(let f=0;f<r.length;f+=1)s.push(w(c,r[f]))}return s}function fe(t,n,o){let r=de(t,o),u=t.text;if(u!==void 0){let s=R(u),l=1/0;if(s===null)l=0;else for(let c=0;c<r.length;c+=1){let f=r[c],h=L(w(s,f),f);h<l&&(l=h)}l<N&&(delete t.text,n.push({key:"text",code:"variable_contrast_too_low"}))}let d=t.accent;if(d!==void 0){let s=R(d),l=1/0;if(s===null)l=0;else if(s.a>=1)l=L(s,H);else for(let c=0;c<r.length;c+=1){let f=L(w(s,r[c]),H);f<l&&(l=f)}l<N&&(delete t.accent,n.push({key:"accent",code:"variable_contrast_too_low"}))}}function pe(t,n){return t==="colour"?R(n)!==null:t==="length"?Q.test(n):ee.test(n)}function ge(t,n){let o=B.get(t);return o===void 0?{code:"unknown_variable",value:""}:typeof n!="string"?{code:"variable_not_a_string",value:""}:n.length>64?{code:"variable_value_too_long",value:""}:o.kind==="colour"&&re.has(t)&&te.has(n.toLowerCase())?{code:"variable_value_erases_content",value:""}:pe(o.kind,n)?{code:null,value:n}:{code:"variable_value_malformed",value:""}}function U(t,n=null){let o={},r=[];if(t===null||typeof t!="object"||Array.isArray(t))return{accepted:{},declarations:{},rejections:[]};let u=Object.keys(t);for(let s=0;s<u.length;s+=1){let l=u[s];if(s>=12){r.push({key:l,code:"too_many_variables"});break}let c=ge(l,t[l]);if(c.code!==null){r.push({key:l,code:c.code});continue}o[l]=c.value}fe(o,r,n);let d={};for(let s of T){let l=o[s];if(l===void 0)continue;let c=B.get(s).targets;for(let f=0;f<c.length;f+=1)d[c[f]]=l}return{accepted:o,declarations:d,rejections:r}}function j(t){return t==="light"||t==="dark"?t:null}var z="4";(function(){"use strict";if(typeof window=="undefined")return;let t="width=460,height=640",n=2500,o="Pay with Canopy",r=500,u=420,d="canopy",s="cnpy_pk_live_";function l(){let e=document.currentScript;if(e&&e.src)try{return new URL(e.src).origin}catch(i){}let a=document.getElementsByTagName("script");for(let i=0;i<a.length;i+=1){let p=a[i].src;if(p&&/\/v1\/(?:[^/]+\/)?canopy\.js(?:[?#].*)?$/.test(p))try{return new URL(p).origin}catch(b){}}return null}function c(e){let a;try{a=new URL(e)}catch(b){return{ok:!1,code:"canopy_origin_invalid"}}if(!((a.pathname===""||a.pathname==="/")&&a.search===""&&a.hash===""))return{ok:!1,code:"canopy_origin_not_bare"};let p=a.hostname==="localhost"||a.hostname==="127.0.0.1";return a.protocol!=="https:"&&!p?{ok:!1,code:"canopy_origin_insecure"}:{ok:!0,origin:a.origin}}function f(e){if(e!==void 0)return c(e);let a=l();return a?{ok:!0,origin:a}:{ok:!1,code:"canopy_origin_undiscoverable"}}function h(e){let a=new URL("/customer-pay",e.origin);a.searchParams.set("merchant",e.merchant),e.theme!==null&&a.searchParams.set(S,e.theme);for(let i=0;i<T.length;i+=1){let p=T[i],b=e.themeVariables[p];b!==void 0&&a.searchParams.set("v."+p,b)}return a.hash="cap="+encodeURIComponent(e.capability||""),a.toString()}function m(e,a,i){if(!e.destroyed)try{e.onEvent(Object.assign({type:a},i))}catch(p){}}function W(e,a){for(let i=0;i<a.length;i+=1){let p=a[i];m(e,"error",{code:p.code,variable:p.key})}}function I(e){e.handshakeTimer&&(clearTimeout(e.handshakeTimer),e.handshakeTimer=null),e.resizeObserver&&(e.resizeObserver.disconnect(),e.resizeObserver=null),e.frame&&(e.frame.parentNode&&e.frame.parentNode.removeChild(e.frame),e.frame=null),e.lastHeight=null}function K(e){e.popupPollTimer&&clearInterval(e.popupPollTimer),e.popupPollTimer=setInterval(function(){try{e.popupWin&&e.popupWin.closed&&(e.popupPollTimer&&clearInterval(e.popupPollTimer),e.popupPollTimer=null,e.popupWin=null,m(e,"close",{via:"popup"}))}catch(a){e.popupPollTimer&&clearInterval(e.popupPollTimer),e.popupPollTimer=null}},r)}function M(e,a){if(!e.destroyed){if(!a){m(e,"error",{code:"popup_blocked"});return}if(!e.capability){try{a.close()}catch(i){}m(e,"error",{code:"capability_unavailable"});return}try{a.location.href=h(e)}catch(i){m(e,"error",{code:"popup_navigation_failed"});return}e.popupWin=a,K(e)}}function G(e){let a=document.createElement("button");a.type="button",a.textContent=e.fallbackLabel;let i=a.style;i.background="#1A7A4C",i.color="#FFFFFF",i.border="none",i.borderRadius="8px",i.height="48px",i.padding="0 20px",i.fontSize="16px",i.fontFamily="inherit",i.cursor="pointer",i.maxWidth="100%",i.overflow="hidden",i.textOverflow="ellipsis",i.whiteSpace="nowrap",i.display="inline-block",a.addEventListener("click",function(){let p=window.open("","canopy_checkout",t);M(e,p)}),e.node.appendChild(a),e.fallbackButton=a,m(e,"fallback_offered",{surface:"popup",reason:"iframe_unavailable"})}function X(e){let a=document.createElement("iframe");a.src=h(e),a.style.width="100%",a.style.border="none",a.setAttribute("allow","clipboard-write"),a.setAttribute("title","Canopy checkout"),a.style.height=u+"px",e.node.appendChild(a),e.frame=a,typeof ResizeObserver!="undefined"&&(e.resizeObserver=new ResizeObserver(function(i){for(let p=0;p<i.length;p+=1){let b=Math.round(i[p].contentRect.height);b>0&&b!==e.lastHeight&&(e.lastHeight=b,m(e,"resize",{height:b}))}}),e.resizeObserver.observe(a)),e.handshakeTimer=setTimeout(function(){e.handshakeTimer=null,m(e,"surface_unavailable",{attempted:"iframe"}),I(e),e.surface==="auto"?G(e):m(e,"error",{code:"iframe_blocked"})},e.handshakeTimeoutMs)}function D(e,a){if(a.origin!==e.origin)return;let i=a.data;if(!(!i||i.source!==d)){if(i.type==="ready"){e.handshakeTimer&&(clearTimeout(e.handshakeTimer),e.handshakeTimer=null),m(e,"ready");return}if(i.type==="resize"&&typeof i.height=="number"&&e.frame){e.frame.style.height=i.height+"px";return}if(i.type==="paid"){m(e,"paid",i);return}if(i.type==="close"){m(e,"close");return}}}let E=new WeakMap;function $(e){if(typeof e.merchant!="string"||e.merchant.indexOf(s)!==0){try{(e.onEvent||function(){})({type:"error",code:"invalid_merchant_key"})}catch(_){}return{openNow:function(){},destroy:function(){}}}let a=f(e.canopyOrigin);if(!a.ok){try{(e.onEvent||function(){})({type:"error",code:a.code})}catch(_){}return{openNow:function(){},destroy:function(){}}}let i=j(e.theme),p=U(e.variables,i),b=typeof e.target=="string"?document.querySelector(e.target):e.target;if(!b)return{openNow:function(){},destroy:function(){}};let V=E.get(b);if(V)return V;let g={node:b,capability:e.capability,merchant:e.merchant,origin:a.origin,theme:i,themeVariables:p.accepted,surface:e.surface||"popup",handshakeTimeoutMs:e.handshakeTimeoutMs||n,fallbackLabel:e.fallbackLabel||o,onEvent:e.onEvent||function(){},destroyed:!1,messageListener:function(){},handshakeTimer:null,resizeObserver:null,frame:null,fallbackButton:null,lastHeight:null,popupWin:null,popupPollTimer:null};g.messageListener=function(_){D(g,_)},window.addEventListener("message",g.messageListener),W(g,p.rejections),(g.surface==="iframe"||g.surface==="auto")&&X(g);let P={openNow:function(_){M(g,_)},destroy:function(){g.destroyed||(g.destroyed=!0,window.removeEventListener("message",g.messageListener),I(g),g.popupPollTimer&&(clearInterval(g.popupPollTimer),g.popupPollTimer=null),g.fallbackButton&&(g.fallbackButton.parentNode&&g.fallbackButton.parentNode.removeChild(g.fallbackButton),g.fallbackButton=null),E.delete(b))}};return E.set(b,P),P}let k=window;k.Canopy&&k.Canopy.__version===z||(k.Canopy={mount:$,__version:z})})();})();
package/dist/index.cjs CHANGED
@@ -25,8 +25,449 @@ __export(src_exports, {
25
25
  });
26
26
  module.exports = __toCommonJS(src_exports);
27
27
 
28
+ // src/theme.ts
29
+ var THEME_PARAM = "theme";
30
+ var THEME_VARIABLE_PARAM_PREFIX = "v.";
31
+ var MAX_THEME_VARIABLE_VALUE_LENGTH = 64;
32
+ var MAX_THEME_VARIABLE_ENTRIES = 12;
33
+ var CANOPY_THEME_VARIABLES = [
34
+ "accent",
35
+ "background",
36
+ "surface",
37
+ "text",
38
+ "muted",
39
+ "border",
40
+ "radius",
41
+ "font"
42
+ ];
43
+ var VARIABLE_TARGETS = /* @__PURE__ */ new Map([
44
+ [
45
+ "accent",
46
+ {
47
+ kind: "colour",
48
+ targets: ["--leaf", "--primary", "--accent", "--ring"]
49
+ }
50
+ ],
51
+ [
52
+ "background",
53
+ {
54
+ kind: "colour",
55
+ targets: ["--paper", "--background", "background"]
56
+ }
57
+ ],
58
+ [
59
+ "surface",
60
+ {
61
+ kind: "colour",
62
+ targets: ["--surface", "--card", "--popover", "--secondary"]
63
+ }
64
+ ],
65
+ [
66
+ "text",
67
+ {
68
+ kind: "colour",
69
+ targets: [
70
+ "--ink",
71
+ "--foreground",
72
+ "--card-foreground",
73
+ "--popover-foreground",
74
+ "--secondary-foreground"
75
+ ]
76
+ }
77
+ ],
78
+ [
79
+ "muted",
80
+ {
81
+ kind: "colour",
82
+ targets: ["--ink-muted", "--muted-foreground"]
83
+ }
84
+ ],
85
+ [
86
+ "border",
87
+ { kind: "colour", targets: ["--line", "--border", "--input"] }
88
+ ],
89
+ ["radius", { kind: "length", targets: ["--radius"] }],
90
+ ["font", { kind: "font", targets: ["--font-sans"] }]
91
+ ]);
92
+ var COLOUR_HEX = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
93
+ var COLOUR_FUNCTION = /^(rgb|rgba|hsl|hsla)\(([0-9a-z.,%/ +-]*)\)$/;
94
+ var NUMBER = /^[+-]?(?:[0-9]{1,4}(?:\.[0-9]{1,4})?|\.[0-9]{1,4})$/;
95
+ var PERCENT = /^[+-]?(?:[0-9]{1,3}(?:\.[0-9]{1,4})?|\.[0-9]{1,4})%$/;
96
+ var LENGTH = /^(?:0|(?:[0-9]{1,4}(?:\.[0-9]{1,4})?|\.[0-9]{1,4})(?:px|rem|em|%))$/;
97
+ var FONT_FAMILY_LIST = /^[A-Za-z0-9 ,_-]+$/;
98
+ var NAMED_COLOURS = /* @__PURE__ */ new Map([
99
+ ["transparent", { r: 0, g: 0, b: 0, a: 0 }],
100
+ ["black", { r: 0, g: 0, b: 0, a: 1 }],
101
+ ["white", { r: 255, g: 255, b: 255, a: 1 }],
102
+ ["silver", { r: 192, g: 192, b: 192, a: 1 }],
103
+ ["gray", { r: 128, g: 128, b: 128, a: 1 }],
104
+ ["grey", { r: 128, g: 128, b: 128, a: 1 }],
105
+ ["maroon", { r: 128, g: 0, b: 0, a: 1 }],
106
+ ["red", { r: 255, g: 0, b: 0, a: 1 }],
107
+ ["purple", { r: 128, g: 0, b: 128, a: 1 }],
108
+ ["fuchsia", { r: 255, g: 0, b: 255, a: 1 }],
109
+ ["magenta", { r: 255, g: 0, b: 255, a: 1 }],
110
+ ["green", { r: 0, g: 128, b: 0, a: 1 }],
111
+ ["lime", { r: 0, g: 255, b: 0, a: 1 }],
112
+ ["olive", { r: 128, g: 128, b: 0, a: 1 }],
113
+ ["yellow", { r: 255, g: 255, b: 0, a: 1 }],
114
+ ["navy", { r: 0, g: 0, b: 128, a: 1 }],
115
+ ["blue", { r: 0, g: 0, b: 255, a: 1 }],
116
+ ["teal", { r: 0, g: 128, b: 128, a: 1 }],
117
+ ["aqua", { r: 0, g: 255, b: 255, a: 1 }],
118
+ ["cyan", { r: 0, g: 255, b: 255, a: 1 }],
119
+ ["orange", { r: 255, g: 165, b: 0, a: 1 }],
120
+ ["pink", { r: 255, g: 192, b: 203, a: 1 }],
121
+ ["brown", { r: 165, g: 42, b: 42, a: 1 }],
122
+ ["beige", { r: 245, g: 245, b: 220, a: 1 }],
123
+ ["ivory", { r: 255, g: 255, b: 240, a: 1 }],
124
+ ["indigo", { r: 75, g: 0, b: 130, a: 1 }],
125
+ ["violet", { r: 238, g: 130, b: 238, a: 1 }],
126
+ ["gold", { r: 255, g: 215, b: 0, a: 1 }],
127
+ ["crimson", { r: 220, g: 20, b: 60, a: 1 }],
128
+ ["salmon", { r: 250, g: 128, b: 114, a: 1 }],
129
+ ["khaki", { r: 240, g: 230, b: 140, a: 1 }],
130
+ ["lavender", { r: 230, g: 230, b: 250, a: 1 }],
131
+ ["turquoise", { r: 64, g: 224, b: 208, a: 1 }],
132
+ ["tan", { r: 210, g: 180, b: 140, a: 1 }],
133
+ ["plum", { r: 221, g: 160, b: 221, a: 1 }],
134
+ ["orchid", { r: 218, g: 112, b: 214, a: 1 }],
135
+ ["coral", { r: 255, g: 127, b: 80, a: 1 }],
136
+ ["tomato", { r: 255, g: 99, b: 71, a: 1 }],
137
+ ["chocolate", { r: 210, g: 105, b: 30, a: 1 }],
138
+ ["snow", { r: 255, g: 250, b: 250, a: 1 }],
139
+ ["linen", { r: 250, g: 240, b: 230, a: 1 }],
140
+ ["azure", { r: 240, g: 255, b: 255, a: 1 }],
141
+ ["wheat", { r: 245, g: 222, b: 179, a: 1 }],
142
+ ["thistle", { r: 216, g: 191, b: 216, a: 1 }]
143
+ ]);
144
+ var CONTENT_ERASING_COLOURS = /* @__PURE__ */ new Set([
145
+ "transparent",
146
+ "currentcolor",
147
+ "inherit",
148
+ "initial",
149
+ "unset",
150
+ "revert",
151
+ "revert-layer"
152
+ ]);
153
+ var FOREGROUND_VARIABLES = /* @__PURE__ */ new Set([
154
+ "text",
155
+ "muted",
156
+ "accent"
157
+ ]);
158
+ function clamp(value, low, high) {
159
+ if (value < low) return low;
160
+ if (value > high) return high;
161
+ return value;
162
+ }
163
+ function parseNumber(token) {
164
+ if (!NUMBER.test(token)) return null;
165
+ const value = Number(token);
166
+ return Number.isFinite(value) ? value : null;
167
+ }
168
+ function parsePercent(token) {
169
+ if (!PERCENT.test(token)) return null;
170
+ const value = Number(token.slice(0, token.length - 1));
171
+ return Number.isFinite(value) ? value / 100 : null;
172
+ }
173
+ function parseRgbComponent(token) {
174
+ const percent = parsePercent(token);
175
+ if (percent !== null) return clamp(percent, 0, 1) * 255;
176
+ const number = parseNumber(token);
177
+ if (number === null) return null;
178
+ return clamp(number, 0, 255);
179
+ }
180
+ function parseAlpha(token) {
181
+ const percent = parsePercent(token);
182
+ if (percent !== null) return clamp(percent, 0, 1);
183
+ const number = parseNumber(token);
184
+ if (number === null) return null;
185
+ return clamp(number, 0, 1);
186
+ }
187
+ function parseHue(token) {
188
+ let text = token;
189
+ let scale = 1;
190
+ if (text.endsWith("deg")) text = text.slice(0, text.length - 3);
191
+ else if (text.endsWith("grad")) {
192
+ text = text.slice(0, text.length - 4);
193
+ scale = 360 / 400;
194
+ } else if (text.endsWith("turn")) {
195
+ text = text.slice(0, text.length - 4);
196
+ scale = 360;
197
+ } else if (text.endsWith("rad")) {
198
+ text = text.slice(0, text.length - 3);
199
+ scale = 180 / Math.PI;
200
+ }
201
+ const number = parseNumber(text);
202
+ if (number === null) return null;
203
+ return (number * scale % 360 + 360) % 360;
204
+ }
205
+ function hueToChannel(p, q, tRaw) {
206
+ let t = tRaw;
207
+ if (t < 0) t += 1;
208
+ if (t > 1) t -= 1;
209
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
210
+ if (t < 1 / 2) return q;
211
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
212
+ return p;
213
+ }
214
+ function hslToRgb(h, s, l, a) {
215
+ if (s === 0) {
216
+ const grey = l * 255;
217
+ return { r: grey, g: grey, b: grey, a };
218
+ }
219
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
220
+ const p = 2 * l - q;
221
+ const hk = h / 360;
222
+ return {
223
+ r: hueToChannel(p, q, hk + 1 / 3) * 255,
224
+ g: hueToChannel(p, q, hk) * 255,
225
+ b: hueToChannel(p, q, hk - 1 / 3) * 255,
226
+ a
227
+ };
228
+ }
229
+ function splitColourComponents(body) {
230
+ const trimmed = body.trim();
231
+ if (trimmed === "") return null;
232
+ if (trimmed.indexOf(",") >= 0) {
233
+ if (trimmed.indexOf("/") >= 0) return null;
234
+ const parts = trimmed.split(",").map(function(part) {
235
+ return part.trim();
236
+ });
237
+ if (parts.length !== 3 && parts.length !== 4) return null;
238
+ return parts;
239
+ }
240
+ const slashParts = trimmed.split("/");
241
+ if (slashParts.length > 2) return null;
242
+ const main = slashParts[0].trim().split(/ +/);
243
+ if (main.length !== 3) return null;
244
+ if (slashParts.length === 1) return main;
245
+ const alpha = slashParts[1].trim();
246
+ if (alpha === "" || alpha.indexOf(" ") >= 0) return null;
247
+ return main.concat([alpha]);
248
+ }
249
+ function parseColourFunction(value) {
250
+ const match = COLOUR_FUNCTION.exec(value);
251
+ if (match === null) return null;
252
+ const fn = match[1];
253
+ const parts = splitColourComponents(match[2]);
254
+ if (parts === null) return null;
255
+ let alpha = 1;
256
+ if (parts.length === 4) {
257
+ const parsed = parseAlpha(parts[3]);
258
+ if (parsed === null) return null;
259
+ alpha = parsed;
260
+ }
261
+ if (fn === "rgb" || fn === "rgba") {
262
+ const r = parseRgbComponent(parts[0]);
263
+ const g = parseRgbComponent(parts[1]);
264
+ const b = parseRgbComponent(parts[2]);
265
+ if (r === null || g === null || b === null) return null;
266
+ return { r, g, b, a: alpha };
267
+ }
268
+ const h = parseHue(parts[0]);
269
+ const s = parsePercent(parts[1]);
270
+ const l = parsePercent(parts[2]);
271
+ if (h === null || s === null || l === null) return null;
272
+ return hslToRgb(h, clamp(s, 0, 1), clamp(l, 0, 1), alpha);
273
+ }
274
+ function parseHexColour(value) {
275
+ if (!COLOUR_HEX.test(value)) return null;
276
+ const digits = value.slice(1);
277
+ const short = digits.length === 3 || digits.length === 4;
278
+ const size = short ? 1 : 2;
279
+ const channel = function(index) {
280
+ const part = digits.slice(index * size, index * size + size);
281
+ return parseInt(short ? part + part : part, 16);
282
+ };
283
+ const hasAlpha = digits.length === 4 || digits.length === 8;
284
+ return {
285
+ r: channel(0),
286
+ g: channel(1),
287
+ b: channel(2),
288
+ a: hasAlpha ? channel(3) / 255 : 1
289
+ };
290
+ }
291
+ function parseColour(value) {
292
+ const hex = parseHexColour(value);
293
+ if (hex !== null) return hex;
294
+ const fn = parseColourFunction(value);
295
+ if (fn !== null) return fn;
296
+ const named = NAMED_COLOURS.get(value.toLowerCase());
297
+ return named === void 0 ? null : named;
298
+ }
299
+ function channelLuminance(component) {
300
+ const s = component / 255;
301
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
302
+ }
303
+ function relativeLuminance(colour) {
304
+ return 0.2126 * channelLuminance(colour.r) + 0.7152 * channelLuminance(colour.g) + 0.0722 * channelLuminance(colour.b);
305
+ }
306
+ function contrastRatio(a, b) {
307
+ const la = relativeLuminance(a);
308
+ const lb = relativeLuminance(b);
309
+ const light = la > lb ? la : lb;
310
+ const dark = la > lb ? lb : la;
311
+ return (light + 0.05) / (dark + 0.05);
312
+ }
313
+ function composite(fg, bg) {
314
+ if (fg.a >= 1) return fg;
315
+ return {
316
+ r: fg.r * fg.a + bg.r * (1 - fg.a),
317
+ g: fg.g * fg.a + bg.g * (1 - fg.a),
318
+ b: fg.b * fg.a + bg.b * (1 - fg.a),
319
+ a: 1
320
+ };
321
+ }
322
+ var MIN_THEME_CONTRAST_RATIO = 3;
323
+ var DEFAULT_BACKDROPS = {
324
+ light: [
325
+ { r: 243, g: 250, b: 246, a: 1 },
326
+ // --paper
327
+ { r: 255, g: 255, b: 255, a: 1 }
328
+ // --surface (--white)
329
+ ],
330
+ dark: [
331
+ { r: 10, g: 22, b: 40, a: 1 },
332
+ // --background (--navy)
333
+ { r: 26, g: 40, b: 60, a: 1 }
334
+ // --surface (--navy-mid)
335
+ ]
336
+ };
337
+ var ACCENT_FOREGROUND = { r: 255, g: 255, b: 255, a: 1 };
338
+ function backdropCandidates(accepted, forcedTheme) {
339
+ const schemes = forcedTheme === null ? ["light", "dark"] : [forcedTheme];
340
+ const bases = [];
341
+ for (let i = 0; i < schemes.length; i += 1) {
342
+ const scheme = DEFAULT_BACKDROPS[schemes[i]];
343
+ for (let j = 0; j < scheme.length; j += 1) bases.push(scheme[j]);
344
+ }
345
+ const supplied = [];
346
+ const names = ["surface", "background"];
347
+ for (let i = 0; i < names.length; i += 1) {
348
+ const raw = accepted[names[i]];
349
+ if (raw === void 0) continue;
350
+ const parsed = parseColour(raw);
351
+ if (parsed === null) continue;
352
+ supplied.push(parsed);
353
+ }
354
+ if (supplied.length === 0) return bases;
355
+ const out = [];
356
+ for (let i = 0; i < supplied.length; i += 1) {
357
+ const value = supplied[i];
358
+ if (value.a >= 1) {
359
+ out.push(value);
360
+ continue;
361
+ }
362
+ for (let j = 0; j < bases.length; j += 1) {
363
+ out.push(composite(value, bases[j]));
364
+ }
365
+ }
366
+ return out;
367
+ }
368
+ function enforceLegibilityFloor(accepted, rejections, forcedTheme) {
369
+ const backdrops = backdropCandidates(accepted, forcedTheme);
370
+ const text = accepted["text"];
371
+ if (text !== void 0) {
372
+ const parsed = parseColour(text);
373
+ let worst = Infinity;
374
+ if (parsed === null) worst = 0;
375
+ else {
376
+ for (let i = 0; i < backdrops.length; i += 1) {
377
+ const backdrop = backdrops[i];
378
+ const ratio = contrastRatio(composite(parsed, backdrop), backdrop);
379
+ if (ratio < worst) worst = ratio;
380
+ }
381
+ }
382
+ if (worst < MIN_THEME_CONTRAST_RATIO) {
383
+ delete accepted["text"];
384
+ rejections.push({ key: "text", code: "variable_contrast_too_low" });
385
+ }
386
+ }
387
+ const accent = accepted["accent"];
388
+ if (accent !== void 0) {
389
+ const parsed = parseColour(accent);
390
+ let worst = Infinity;
391
+ if (parsed === null) worst = 0;
392
+ else if (parsed.a >= 1) {
393
+ worst = contrastRatio(parsed, ACCENT_FOREGROUND);
394
+ } else {
395
+ for (let i = 0; i < backdrops.length; i += 1) {
396
+ const ratio = contrastRatio(
397
+ composite(parsed, backdrops[i]),
398
+ ACCENT_FOREGROUND
399
+ );
400
+ if (ratio < worst) worst = ratio;
401
+ }
402
+ }
403
+ if (worst < MIN_THEME_CONTRAST_RATIO) {
404
+ delete accepted["accent"];
405
+ rejections.push({ key: "accent", code: "variable_contrast_too_low" });
406
+ }
407
+ }
408
+ }
409
+ function matchesKind(kind, value) {
410
+ if (kind === "colour") return parseColour(value) !== null;
411
+ if (kind === "length") return LENGTH.test(value);
412
+ return FONT_FAMILY_LIST.test(value);
413
+ }
414
+ function classifyEntry(key, raw) {
415
+ const spec = VARIABLE_TARGETS.get(key);
416
+ if (spec === void 0) return { code: "unknown_variable", value: "" };
417
+ if (typeof raw !== "string")
418
+ return { code: "variable_not_a_string", value: "" };
419
+ if (raw.length > MAX_THEME_VARIABLE_VALUE_LENGTH)
420
+ return { code: "variable_value_too_long", value: "" };
421
+ if (spec.kind === "colour" && FOREGROUND_VARIABLES.has(key) && CONTENT_ERASING_COLOURS.has(raw.toLowerCase())) {
422
+ return { code: "variable_value_erases_content", value: "" };
423
+ }
424
+ if (!matchesKind(spec.kind, raw))
425
+ return { code: "variable_value_malformed", value: "" };
426
+ return { code: null, value: raw };
427
+ }
428
+ function resolveThemeVariables(raw, forcedTheme = null) {
429
+ const accepted = {};
430
+ const rejections = [];
431
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
432
+ return { accepted: {}, declarations: {}, rejections: [] };
433
+ }
434
+ const entries = Object.keys(raw);
435
+ for (let i = 0; i < entries.length; i += 1) {
436
+ const key = entries[i];
437
+ if (i >= MAX_THEME_VARIABLE_ENTRIES) {
438
+ rejections.push({ key, code: "too_many_variables" });
439
+ break;
440
+ }
441
+ const outcome = classifyEntry(key, raw[key]);
442
+ if (outcome.code !== null) {
443
+ rejections.push({ key, code: outcome.code });
444
+ continue;
445
+ }
446
+ accepted[key] = outcome.value;
447
+ }
448
+ enforceLegibilityFloor(accepted, rejections, forcedTheme);
449
+ const declarations = {};
450
+ for (const name of CANOPY_THEME_VARIABLES) {
451
+ const value = accepted[name];
452
+ if (value === void 0) continue;
453
+ const targets = VARIABLE_TARGETS.get(name).targets;
454
+ for (let i = 0; i < targets.length; i += 1) {
455
+ declarations[targets[i]] = value;
456
+ }
457
+ }
458
+ return {
459
+ accepted,
460
+ declarations,
461
+ rejections
462
+ };
463
+ }
464
+ function normalizeForcedTheme(raw) {
465
+ if (raw === "light" || raw === "dark") return raw;
466
+ return null;
467
+ }
468
+
28
469
  // src/loader.ts
29
- var LOADER_VERSION = "3";
470
+ var LOADER_VERSION = "4";
30
471
  (function() {
31
472
  "use strict";
32
473
  if (typeof window === "undefined") return;
@@ -87,6 +528,15 @@ var LOADER_VERSION = "3";
87
528
  function buildCheckoutUrl(internal) {
88
529
  const url = new URL("/customer-pay", internal.origin);
89
530
  url.searchParams.set("merchant", internal.merchant);
531
+ if (internal.theme !== null) {
532
+ url.searchParams.set(THEME_PARAM, internal.theme);
533
+ }
534
+ for (let i = 0; i < CANOPY_THEME_VARIABLES.length; i += 1) {
535
+ const name = CANOPY_THEME_VARIABLES[i];
536
+ const value = internal.themeVariables[name];
537
+ if (value === void 0) continue;
538
+ url.searchParams.set(THEME_VARIABLE_PARAM_PREFIX + name, value);
539
+ }
90
540
  url.hash = "cap=" + encodeURIComponent(internal.capability || "");
91
541
  return url.toString();
92
542
  }
@@ -97,6 +547,15 @@ var LOADER_VERSION = "3";
97
547
  } catch {
98
548
  }
99
549
  }
550
+ function emitThemeRejections(internal, rejections) {
551
+ for (let i = 0; i < rejections.length; i += 1) {
552
+ const rejection = rejections[i];
553
+ emit(internal, "error", {
554
+ code: rejection.code,
555
+ variable: rejection.key
556
+ });
557
+ }
558
+ }
100
559
  function teardownFrame(internal) {
101
560
  if (internal.handshakeTimer) {
102
561
  clearTimeout(internal.handshakeTimer);
@@ -269,6 +728,8 @@ var LOADER_VERSION = "3";
269
728
  }, destroy: function() {
270
729
  } };
271
730
  }
731
+ const forcedTheme = normalizeForcedTheme(opts.theme);
732
+ const themeVariables = resolveThemeVariables(opts.variables, forcedTheme);
272
733
  const node = typeof opts.target === "string" ? document.querySelector(opts.target) : opts.target;
273
734
  if (!node) {
274
735
  return { openNow: function() {
@@ -282,6 +743,8 @@ var LOADER_VERSION = "3";
282
743
  capability: opts.capability,
283
744
  merchant: opts.merchant,
284
745
  origin: originResolution.origin,
746
+ theme: forcedTheme,
747
+ themeVariables: themeVariables.accepted,
285
748
  surface: opts.surface || "popup",
286
749
  handshakeTimeoutMs: opts.handshakeTimeoutMs || DEFAULT_HANDSHAKE_TIMEOUT_MS,
287
750
  fallbackLabel: opts.fallbackLabel || DEFAULT_FALLBACK_LABEL,
@@ -302,6 +765,7 @@ var LOADER_VERSION = "3";
302
765
  handleMessage(internal, event);
303
766
  };
304
767
  window.addEventListener("message", internal.messageListener);
768
+ emitThemeRejections(internal, themeVariables.rejections);
305
769
  if (internal.surface === "iframe" || internal.surface === "auto") {
306
770
  createFrame(internal);
307
771
  }
package/dist/index.d.cts CHANGED
@@ -1,3 +1,38 @@
1
+ /**
2
+ * Canopy checkout theming -- the ONE validator behind
3
+ * `Canopy.mount({ theme, variables })`.
4
+ *
5
+ * Imported by BOTH `packages/checkout-sdk/src/loader.ts` (tsup bundles this
6
+ * relative sibling into the IIFE) and `app/customer-pay/page.tsx` (the app
7
+ * already imports across this package boundary; `tsconfig.json` maps `@/*`
8
+ * to the repo root and includes `packages/**\/*.ts`). Deliberately NOT
9
+ * duplicated the way `MESSAGE_SOURCE` is -- that duplication exists only
10
+ * because a React component cannot reach into the tsup entry graph, which
11
+ * is not the case here. Two copies of a grammar are two grammars.
12
+ *
13
+ * The two passes are not equals. The SERVER pass in `app/customer-pay/
14
+ * page.tsx` is the security boundary: those values arrive as query
15
+ * parameters that anyone can hand-craft, with no loader involved. The
16
+ * client-side pass inside `mount()` is a developer-experience courtesy --
17
+ * it turns a typo into an `error` event on the merchant's own console
18
+ * instead of a value that silently vanishes server-side.
19
+ *
20
+ * Threat register (quick 260906-qql): T-QQL-01 tampering (a value escaping
21
+ * its declaration), T-QQL-02 elevation (a prototype-chain key), T-QQL-03
22
+ * denial of service (unbounded input).
23
+ *
24
+ * Pure and browser-free: no `window`, no `document`, no DOM types. It runs
25
+ * inside a React Server Component, inside a `node --test` process, and
26
+ * inside an es2018 IIFE in a merchant's page.
27
+ */
28
+ /** What a merchant may pass as `theme`. */
29
+ type CanopyThemeOption = "light" | "dark" | "auto";
30
+ /** The eight public names, in the order their declarations are emitted. */
31
+ declare const CANOPY_THEME_VARIABLES: readonly ["accent", "background", "surface", "text", "muted", "border", "radius", "font"];
32
+ type CanopyThemeVariable = (typeof CANOPY_THEME_VARIABLES)[number];
33
+ /** What a merchant may pass as `variables`. */
34
+ type CanopyThemeVariables = Partial<Record<CanopyThemeVariable, string>>;
35
+
1
36
  /**
2
37
  * Canopy checkout SDK loader.
3
38
  *
@@ -31,6 +66,7 @@
31
66
  * | "auto"`, default `"popup"`), seven events (`ready`, `resize`, `paid`,
32
67
  * `error`, `close`, `surface_unavailable`, `fallback_offered`).
33
68
  */
69
+
34
70
  /**
35
71
  * `close` is POPUP-ONLY. It reports that the payer closed the checkout
36
72
  * window, which the loader detects by polling `popupWin.closed`. An inline
@@ -68,6 +104,28 @@ type MountOptions = {
68
104
  * script tag when both are available.
69
105
  */
70
106
  canopyOrigin?: string;
107
+ /**
108
+ * Forces the checkout's colour scheme for THIS embed only, overriding the
109
+ * visitor's own stored preference inside the frame and nowhere else.
110
+ * `"auto"` and omitting it are the same thing and are the default: the
111
+ * visitor's preference (and their OS setting) keeps deciding. Nothing is
112
+ * written to the visitor's storage either way.
113
+ */
114
+ theme?: CanopyThemeOption;
115
+ /**
116
+ * Repaints the checkout through eight allowlisted names: `accent`,
117
+ * `background`, `surface`, `text`, `muted`, `border`, `radius` and
118
+ * `font`. Values are CSS -- a colour (hex, `rgb()`/`rgba()`/`hsl()`, or a
119
+ * bare keyword such as `transparent`) for the six colour names, a length
120
+ * for `radius`, a plain family list for `font`.
121
+ *
122
+ * Rejection is PER ENTRY and never fatal: an unknown name or a malformed
123
+ * value drops that one entry, emits an `error` event naming it, and lets
124
+ * every other entry through. A cosmetic option can never block a checkout.
125
+ * Validated again server-side, which is the authoritative pass -- this one
126
+ * exists so a typo reaches your console instead of vanishing silently.
127
+ */
128
+ variables?: CanopyThemeVariables;
71
129
  };
72
130
  type CanopyHandle = {
73
131
  openNow: (win: Window | null) => void;
@@ -97,4 +155,4 @@ declare function mount(opts: MountOptions): CanopyHandle;
97
155
  * `undefined` server-side / before the loader has installed the global. */
98
156
  declare function version(): string | undefined;
99
157
 
100
- export { type CanopyEvent, type CanopyEventType, type CanopyHandle, type MountOptions, type Surface, mount, version };
158
+ export { type CanopyEvent, type CanopyEventType, type CanopyHandle, type CanopyThemeOption, type CanopyThemeVariable, type CanopyThemeVariables, type MountOptions, type Surface, mount, version };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,38 @@
1
+ /**
2
+ * Canopy checkout theming -- the ONE validator behind
3
+ * `Canopy.mount({ theme, variables })`.
4
+ *
5
+ * Imported by BOTH `packages/checkout-sdk/src/loader.ts` (tsup bundles this
6
+ * relative sibling into the IIFE) and `app/customer-pay/page.tsx` (the app
7
+ * already imports across this package boundary; `tsconfig.json` maps `@/*`
8
+ * to the repo root and includes `packages/**\/*.ts`). Deliberately NOT
9
+ * duplicated the way `MESSAGE_SOURCE` is -- that duplication exists only
10
+ * because a React component cannot reach into the tsup entry graph, which
11
+ * is not the case here. Two copies of a grammar are two grammars.
12
+ *
13
+ * The two passes are not equals. The SERVER pass in `app/customer-pay/
14
+ * page.tsx` is the security boundary: those values arrive as query
15
+ * parameters that anyone can hand-craft, with no loader involved. The
16
+ * client-side pass inside `mount()` is a developer-experience courtesy --
17
+ * it turns a typo into an `error` event on the merchant's own console
18
+ * instead of a value that silently vanishes server-side.
19
+ *
20
+ * Threat register (quick 260906-qql): T-QQL-01 tampering (a value escaping
21
+ * its declaration), T-QQL-02 elevation (a prototype-chain key), T-QQL-03
22
+ * denial of service (unbounded input).
23
+ *
24
+ * Pure and browser-free: no `window`, no `document`, no DOM types. It runs
25
+ * inside a React Server Component, inside a `node --test` process, and
26
+ * inside an es2018 IIFE in a merchant's page.
27
+ */
28
+ /** What a merchant may pass as `theme`. */
29
+ type CanopyThemeOption = "light" | "dark" | "auto";
30
+ /** The eight public names, in the order their declarations are emitted. */
31
+ declare const CANOPY_THEME_VARIABLES: readonly ["accent", "background", "surface", "text", "muted", "border", "radius", "font"];
32
+ type CanopyThemeVariable = (typeof CANOPY_THEME_VARIABLES)[number];
33
+ /** What a merchant may pass as `variables`. */
34
+ type CanopyThemeVariables = Partial<Record<CanopyThemeVariable, string>>;
35
+
1
36
  /**
2
37
  * Canopy checkout SDK loader.
3
38
  *
@@ -31,6 +66,7 @@
31
66
  * | "auto"`, default `"popup"`), seven events (`ready`, `resize`, `paid`,
32
67
  * `error`, `close`, `surface_unavailable`, `fallback_offered`).
33
68
  */
69
+
34
70
  /**
35
71
  * `close` is POPUP-ONLY. It reports that the payer closed the checkout
36
72
  * window, which the loader detects by polling `popupWin.closed`. An inline
@@ -68,6 +104,28 @@ type MountOptions = {
68
104
  * script tag when both are available.
69
105
  */
70
106
  canopyOrigin?: string;
107
+ /**
108
+ * Forces the checkout's colour scheme for THIS embed only, overriding the
109
+ * visitor's own stored preference inside the frame and nowhere else.
110
+ * `"auto"` and omitting it are the same thing and are the default: the
111
+ * visitor's preference (and their OS setting) keeps deciding. Nothing is
112
+ * written to the visitor's storage either way.
113
+ */
114
+ theme?: CanopyThemeOption;
115
+ /**
116
+ * Repaints the checkout through eight allowlisted names: `accent`,
117
+ * `background`, `surface`, `text`, `muted`, `border`, `radius` and
118
+ * `font`. Values are CSS -- a colour (hex, `rgb()`/`rgba()`/`hsl()`, or a
119
+ * bare keyword such as `transparent`) for the six colour names, a length
120
+ * for `radius`, a plain family list for `font`.
121
+ *
122
+ * Rejection is PER ENTRY and never fatal: an unknown name or a malformed
123
+ * value drops that one entry, emits an `error` event naming it, and lets
124
+ * every other entry through. A cosmetic option can never block a checkout.
125
+ * Validated again server-side, which is the authoritative pass -- this one
126
+ * exists so a typo reaches your console instead of vanishing silently.
127
+ */
128
+ variables?: CanopyThemeVariables;
71
129
  };
72
130
  type CanopyHandle = {
73
131
  openNow: (win: Window | null) => void;
@@ -97,4 +155,4 @@ declare function mount(opts: MountOptions): CanopyHandle;
97
155
  * `undefined` server-side / before the loader has installed the global. */
98
156
  declare function version(): string | undefined;
99
157
 
100
- export { type CanopyEvent, type CanopyEventType, type CanopyHandle, type MountOptions, type Surface, mount, version };
158
+ export { type CanopyEvent, type CanopyEventType, type CanopyHandle, type CanopyThemeOption, type CanopyThemeVariable, type CanopyThemeVariables, type MountOptions, type Surface, mount, version };
package/dist/index.js CHANGED
@@ -1,5 +1,446 @@
1
+ // src/theme.ts
2
+ var THEME_PARAM = "theme";
3
+ var THEME_VARIABLE_PARAM_PREFIX = "v.";
4
+ var MAX_THEME_VARIABLE_VALUE_LENGTH = 64;
5
+ var MAX_THEME_VARIABLE_ENTRIES = 12;
6
+ var CANOPY_THEME_VARIABLES = [
7
+ "accent",
8
+ "background",
9
+ "surface",
10
+ "text",
11
+ "muted",
12
+ "border",
13
+ "radius",
14
+ "font"
15
+ ];
16
+ var VARIABLE_TARGETS = /* @__PURE__ */ new Map([
17
+ [
18
+ "accent",
19
+ {
20
+ kind: "colour",
21
+ targets: ["--leaf", "--primary", "--accent", "--ring"]
22
+ }
23
+ ],
24
+ [
25
+ "background",
26
+ {
27
+ kind: "colour",
28
+ targets: ["--paper", "--background", "background"]
29
+ }
30
+ ],
31
+ [
32
+ "surface",
33
+ {
34
+ kind: "colour",
35
+ targets: ["--surface", "--card", "--popover", "--secondary"]
36
+ }
37
+ ],
38
+ [
39
+ "text",
40
+ {
41
+ kind: "colour",
42
+ targets: [
43
+ "--ink",
44
+ "--foreground",
45
+ "--card-foreground",
46
+ "--popover-foreground",
47
+ "--secondary-foreground"
48
+ ]
49
+ }
50
+ ],
51
+ [
52
+ "muted",
53
+ {
54
+ kind: "colour",
55
+ targets: ["--ink-muted", "--muted-foreground"]
56
+ }
57
+ ],
58
+ [
59
+ "border",
60
+ { kind: "colour", targets: ["--line", "--border", "--input"] }
61
+ ],
62
+ ["radius", { kind: "length", targets: ["--radius"] }],
63
+ ["font", { kind: "font", targets: ["--font-sans"] }]
64
+ ]);
65
+ var COLOUR_HEX = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
66
+ var COLOUR_FUNCTION = /^(rgb|rgba|hsl|hsla)\(([0-9a-z.,%/ +-]*)\)$/;
67
+ var NUMBER = /^[+-]?(?:[0-9]{1,4}(?:\.[0-9]{1,4})?|\.[0-9]{1,4})$/;
68
+ var PERCENT = /^[+-]?(?:[0-9]{1,3}(?:\.[0-9]{1,4})?|\.[0-9]{1,4})%$/;
69
+ var LENGTH = /^(?:0|(?:[0-9]{1,4}(?:\.[0-9]{1,4})?|\.[0-9]{1,4})(?:px|rem|em|%))$/;
70
+ var FONT_FAMILY_LIST = /^[A-Za-z0-9 ,_-]+$/;
71
+ var NAMED_COLOURS = /* @__PURE__ */ new Map([
72
+ ["transparent", { r: 0, g: 0, b: 0, a: 0 }],
73
+ ["black", { r: 0, g: 0, b: 0, a: 1 }],
74
+ ["white", { r: 255, g: 255, b: 255, a: 1 }],
75
+ ["silver", { r: 192, g: 192, b: 192, a: 1 }],
76
+ ["gray", { r: 128, g: 128, b: 128, a: 1 }],
77
+ ["grey", { r: 128, g: 128, b: 128, a: 1 }],
78
+ ["maroon", { r: 128, g: 0, b: 0, a: 1 }],
79
+ ["red", { r: 255, g: 0, b: 0, a: 1 }],
80
+ ["purple", { r: 128, g: 0, b: 128, a: 1 }],
81
+ ["fuchsia", { r: 255, g: 0, b: 255, a: 1 }],
82
+ ["magenta", { r: 255, g: 0, b: 255, a: 1 }],
83
+ ["green", { r: 0, g: 128, b: 0, a: 1 }],
84
+ ["lime", { r: 0, g: 255, b: 0, a: 1 }],
85
+ ["olive", { r: 128, g: 128, b: 0, a: 1 }],
86
+ ["yellow", { r: 255, g: 255, b: 0, a: 1 }],
87
+ ["navy", { r: 0, g: 0, b: 128, a: 1 }],
88
+ ["blue", { r: 0, g: 0, b: 255, a: 1 }],
89
+ ["teal", { r: 0, g: 128, b: 128, a: 1 }],
90
+ ["aqua", { r: 0, g: 255, b: 255, a: 1 }],
91
+ ["cyan", { r: 0, g: 255, b: 255, a: 1 }],
92
+ ["orange", { r: 255, g: 165, b: 0, a: 1 }],
93
+ ["pink", { r: 255, g: 192, b: 203, a: 1 }],
94
+ ["brown", { r: 165, g: 42, b: 42, a: 1 }],
95
+ ["beige", { r: 245, g: 245, b: 220, a: 1 }],
96
+ ["ivory", { r: 255, g: 255, b: 240, a: 1 }],
97
+ ["indigo", { r: 75, g: 0, b: 130, a: 1 }],
98
+ ["violet", { r: 238, g: 130, b: 238, a: 1 }],
99
+ ["gold", { r: 255, g: 215, b: 0, a: 1 }],
100
+ ["crimson", { r: 220, g: 20, b: 60, a: 1 }],
101
+ ["salmon", { r: 250, g: 128, b: 114, a: 1 }],
102
+ ["khaki", { r: 240, g: 230, b: 140, a: 1 }],
103
+ ["lavender", { r: 230, g: 230, b: 250, a: 1 }],
104
+ ["turquoise", { r: 64, g: 224, b: 208, a: 1 }],
105
+ ["tan", { r: 210, g: 180, b: 140, a: 1 }],
106
+ ["plum", { r: 221, g: 160, b: 221, a: 1 }],
107
+ ["orchid", { r: 218, g: 112, b: 214, a: 1 }],
108
+ ["coral", { r: 255, g: 127, b: 80, a: 1 }],
109
+ ["tomato", { r: 255, g: 99, b: 71, a: 1 }],
110
+ ["chocolate", { r: 210, g: 105, b: 30, a: 1 }],
111
+ ["snow", { r: 255, g: 250, b: 250, a: 1 }],
112
+ ["linen", { r: 250, g: 240, b: 230, a: 1 }],
113
+ ["azure", { r: 240, g: 255, b: 255, a: 1 }],
114
+ ["wheat", { r: 245, g: 222, b: 179, a: 1 }],
115
+ ["thistle", { r: 216, g: 191, b: 216, a: 1 }]
116
+ ]);
117
+ var CONTENT_ERASING_COLOURS = /* @__PURE__ */ new Set([
118
+ "transparent",
119
+ "currentcolor",
120
+ "inherit",
121
+ "initial",
122
+ "unset",
123
+ "revert",
124
+ "revert-layer"
125
+ ]);
126
+ var FOREGROUND_VARIABLES = /* @__PURE__ */ new Set([
127
+ "text",
128
+ "muted",
129
+ "accent"
130
+ ]);
131
+ function clamp(value, low, high) {
132
+ if (value < low) return low;
133
+ if (value > high) return high;
134
+ return value;
135
+ }
136
+ function parseNumber(token) {
137
+ if (!NUMBER.test(token)) return null;
138
+ const value = Number(token);
139
+ return Number.isFinite(value) ? value : null;
140
+ }
141
+ function parsePercent(token) {
142
+ if (!PERCENT.test(token)) return null;
143
+ const value = Number(token.slice(0, token.length - 1));
144
+ return Number.isFinite(value) ? value / 100 : null;
145
+ }
146
+ function parseRgbComponent(token) {
147
+ const percent = parsePercent(token);
148
+ if (percent !== null) return clamp(percent, 0, 1) * 255;
149
+ const number = parseNumber(token);
150
+ if (number === null) return null;
151
+ return clamp(number, 0, 255);
152
+ }
153
+ function parseAlpha(token) {
154
+ const percent = parsePercent(token);
155
+ if (percent !== null) return clamp(percent, 0, 1);
156
+ const number = parseNumber(token);
157
+ if (number === null) return null;
158
+ return clamp(number, 0, 1);
159
+ }
160
+ function parseHue(token) {
161
+ let text = token;
162
+ let scale = 1;
163
+ if (text.endsWith("deg")) text = text.slice(0, text.length - 3);
164
+ else if (text.endsWith("grad")) {
165
+ text = text.slice(0, text.length - 4);
166
+ scale = 360 / 400;
167
+ } else if (text.endsWith("turn")) {
168
+ text = text.slice(0, text.length - 4);
169
+ scale = 360;
170
+ } else if (text.endsWith("rad")) {
171
+ text = text.slice(0, text.length - 3);
172
+ scale = 180 / Math.PI;
173
+ }
174
+ const number = parseNumber(text);
175
+ if (number === null) return null;
176
+ return (number * scale % 360 + 360) % 360;
177
+ }
178
+ function hueToChannel(p, q, tRaw) {
179
+ let t = tRaw;
180
+ if (t < 0) t += 1;
181
+ if (t > 1) t -= 1;
182
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
183
+ if (t < 1 / 2) return q;
184
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
185
+ return p;
186
+ }
187
+ function hslToRgb(h, s, l, a) {
188
+ if (s === 0) {
189
+ const grey = l * 255;
190
+ return { r: grey, g: grey, b: grey, a };
191
+ }
192
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
193
+ const p = 2 * l - q;
194
+ const hk = h / 360;
195
+ return {
196
+ r: hueToChannel(p, q, hk + 1 / 3) * 255,
197
+ g: hueToChannel(p, q, hk) * 255,
198
+ b: hueToChannel(p, q, hk - 1 / 3) * 255,
199
+ a
200
+ };
201
+ }
202
+ function splitColourComponents(body) {
203
+ const trimmed = body.trim();
204
+ if (trimmed === "") return null;
205
+ if (trimmed.indexOf(",") >= 0) {
206
+ if (trimmed.indexOf("/") >= 0) return null;
207
+ const parts = trimmed.split(",").map(function(part) {
208
+ return part.trim();
209
+ });
210
+ if (parts.length !== 3 && parts.length !== 4) return null;
211
+ return parts;
212
+ }
213
+ const slashParts = trimmed.split("/");
214
+ if (slashParts.length > 2) return null;
215
+ const main = slashParts[0].trim().split(/ +/);
216
+ if (main.length !== 3) return null;
217
+ if (slashParts.length === 1) return main;
218
+ const alpha = slashParts[1].trim();
219
+ if (alpha === "" || alpha.indexOf(" ") >= 0) return null;
220
+ return main.concat([alpha]);
221
+ }
222
+ function parseColourFunction(value) {
223
+ const match = COLOUR_FUNCTION.exec(value);
224
+ if (match === null) return null;
225
+ const fn = match[1];
226
+ const parts = splitColourComponents(match[2]);
227
+ if (parts === null) return null;
228
+ let alpha = 1;
229
+ if (parts.length === 4) {
230
+ const parsed = parseAlpha(parts[3]);
231
+ if (parsed === null) return null;
232
+ alpha = parsed;
233
+ }
234
+ if (fn === "rgb" || fn === "rgba") {
235
+ const r = parseRgbComponent(parts[0]);
236
+ const g = parseRgbComponent(parts[1]);
237
+ const b = parseRgbComponent(parts[2]);
238
+ if (r === null || g === null || b === null) return null;
239
+ return { r, g, b, a: alpha };
240
+ }
241
+ const h = parseHue(parts[0]);
242
+ const s = parsePercent(parts[1]);
243
+ const l = parsePercent(parts[2]);
244
+ if (h === null || s === null || l === null) return null;
245
+ return hslToRgb(h, clamp(s, 0, 1), clamp(l, 0, 1), alpha);
246
+ }
247
+ function parseHexColour(value) {
248
+ if (!COLOUR_HEX.test(value)) return null;
249
+ const digits = value.slice(1);
250
+ const short = digits.length === 3 || digits.length === 4;
251
+ const size = short ? 1 : 2;
252
+ const channel = function(index) {
253
+ const part = digits.slice(index * size, index * size + size);
254
+ return parseInt(short ? part + part : part, 16);
255
+ };
256
+ const hasAlpha = digits.length === 4 || digits.length === 8;
257
+ return {
258
+ r: channel(0),
259
+ g: channel(1),
260
+ b: channel(2),
261
+ a: hasAlpha ? channel(3) / 255 : 1
262
+ };
263
+ }
264
+ function parseColour(value) {
265
+ const hex = parseHexColour(value);
266
+ if (hex !== null) return hex;
267
+ const fn = parseColourFunction(value);
268
+ if (fn !== null) return fn;
269
+ const named = NAMED_COLOURS.get(value.toLowerCase());
270
+ return named === void 0 ? null : named;
271
+ }
272
+ function channelLuminance(component) {
273
+ const s = component / 255;
274
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
275
+ }
276
+ function relativeLuminance(colour) {
277
+ return 0.2126 * channelLuminance(colour.r) + 0.7152 * channelLuminance(colour.g) + 0.0722 * channelLuminance(colour.b);
278
+ }
279
+ function contrastRatio(a, b) {
280
+ const la = relativeLuminance(a);
281
+ const lb = relativeLuminance(b);
282
+ const light = la > lb ? la : lb;
283
+ const dark = la > lb ? lb : la;
284
+ return (light + 0.05) / (dark + 0.05);
285
+ }
286
+ function composite(fg, bg) {
287
+ if (fg.a >= 1) return fg;
288
+ return {
289
+ r: fg.r * fg.a + bg.r * (1 - fg.a),
290
+ g: fg.g * fg.a + bg.g * (1 - fg.a),
291
+ b: fg.b * fg.a + bg.b * (1 - fg.a),
292
+ a: 1
293
+ };
294
+ }
295
+ var MIN_THEME_CONTRAST_RATIO = 3;
296
+ var DEFAULT_BACKDROPS = {
297
+ light: [
298
+ { r: 243, g: 250, b: 246, a: 1 },
299
+ // --paper
300
+ { r: 255, g: 255, b: 255, a: 1 }
301
+ // --surface (--white)
302
+ ],
303
+ dark: [
304
+ { r: 10, g: 22, b: 40, a: 1 },
305
+ // --background (--navy)
306
+ { r: 26, g: 40, b: 60, a: 1 }
307
+ // --surface (--navy-mid)
308
+ ]
309
+ };
310
+ var ACCENT_FOREGROUND = { r: 255, g: 255, b: 255, a: 1 };
311
+ function backdropCandidates(accepted, forcedTheme) {
312
+ const schemes = forcedTheme === null ? ["light", "dark"] : [forcedTheme];
313
+ const bases = [];
314
+ for (let i = 0; i < schemes.length; i += 1) {
315
+ const scheme = DEFAULT_BACKDROPS[schemes[i]];
316
+ for (let j = 0; j < scheme.length; j += 1) bases.push(scheme[j]);
317
+ }
318
+ const supplied = [];
319
+ const names = ["surface", "background"];
320
+ for (let i = 0; i < names.length; i += 1) {
321
+ const raw = accepted[names[i]];
322
+ if (raw === void 0) continue;
323
+ const parsed = parseColour(raw);
324
+ if (parsed === null) continue;
325
+ supplied.push(parsed);
326
+ }
327
+ if (supplied.length === 0) return bases;
328
+ const out = [];
329
+ for (let i = 0; i < supplied.length; i += 1) {
330
+ const value = supplied[i];
331
+ if (value.a >= 1) {
332
+ out.push(value);
333
+ continue;
334
+ }
335
+ for (let j = 0; j < bases.length; j += 1) {
336
+ out.push(composite(value, bases[j]));
337
+ }
338
+ }
339
+ return out;
340
+ }
341
+ function enforceLegibilityFloor(accepted, rejections, forcedTheme) {
342
+ const backdrops = backdropCandidates(accepted, forcedTheme);
343
+ const text = accepted["text"];
344
+ if (text !== void 0) {
345
+ const parsed = parseColour(text);
346
+ let worst = Infinity;
347
+ if (parsed === null) worst = 0;
348
+ else {
349
+ for (let i = 0; i < backdrops.length; i += 1) {
350
+ const backdrop = backdrops[i];
351
+ const ratio = contrastRatio(composite(parsed, backdrop), backdrop);
352
+ if (ratio < worst) worst = ratio;
353
+ }
354
+ }
355
+ if (worst < MIN_THEME_CONTRAST_RATIO) {
356
+ delete accepted["text"];
357
+ rejections.push({ key: "text", code: "variable_contrast_too_low" });
358
+ }
359
+ }
360
+ const accent = accepted["accent"];
361
+ if (accent !== void 0) {
362
+ const parsed = parseColour(accent);
363
+ let worst = Infinity;
364
+ if (parsed === null) worst = 0;
365
+ else if (parsed.a >= 1) {
366
+ worst = contrastRatio(parsed, ACCENT_FOREGROUND);
367
+ } else {
368
+ for (let i = 0; i < backdrops.length; i += 1) {
369
+ const ratio = contrastRatio(
370
+ composite(parsed, backdrops[i]),
371
+ ACCENT_FOREGROUND
372
+ );
373
+ if (ratio < worst) worst = ratio;
374
+ }
375
+ }
376
+ if (worst < MIN_THEME_CONTRAST_RATIO) {
377
+ delete accepted["accent"];
378
+ rejections.push({ key: "accent", code: "variable_contrast_too_low" });
379
+ }
380
+ }
381
+ }
382
+ function matchesKind(kind, value) {
383
+ if (kind === "colour") return parseColour(value) !== null;
384
+ if (kind === "length") return LENGTH.test(value);
385
+ return FONT_FAMILY_LIST.test(value);
386
+ }
387
+ function classifyEntry(key, raw) {
388
+ const spec = VARIABLE_TARGETS.get(key);
389
+ if (spec === void 0) return { code: "unknown_variable", value: "" };
390
+ if (typeof raw !== "string")
391
+ return { code: "variable_not_a_string", value: "" };
392
+ if (raw.length > MAX_THEME_VARIABLE_VALUE_LENGTH)
393
+ return { code: "variable_value_too_long", value: "" };
394
+ if (spec.kind === "colour" && FOREGROUND_VARIABLES.has(key) && CONTENT_ERASING_COLOURS.has(raw.toLowerCase())) {
395
+ return { code: "variable_value_erases_content", value: "" };
396
+ }
397
+ if (!matchesKind(spec.kind, raw))
398
+ return { code: "variable_value_malformed", value: "" };
399
+ return { code: null, value: raw };
400
+ }
401
+ function resolveThemeVariables(raw, forcedTheme = null) {
402
+ const accepted = {};
403
+ const rejections = [];
404
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
405
+ return { accepted: {}, declarations: {}, rejections: [] };
406
+ }
407
+ const entries = Object.keys(raw);
408
+ for (let i = 0; i < entries.length; i += 1) {
409
+ const key = entries[i];
410
+ if (i >= MAX_THEME_VARIABLE_ENTRIES) {
411
+ rejections.push({ key, code: "too_many_variables" });
412
+ break;
413
+ }
414
+ const outcome = classifyEntry(key, raw[key]);
415
+ if (outcome.code !== null) {
416
+ rejections.push({ key, code: outcome.code });
417
+ continue;
418
+ }
419
+ accepted[key] = outcome.value;
420
+ }
421
+ enforceLegibilityFloor(accepted, rejections, forcedTheme);
422
+ const declarations = {};
423
+ for (const name of CANOPY_THEME_VARIABLES) {
424
+ const value = accepted[name];
425
+ if (value === void 0) continue;
426
+ const targets = VARIABLE_TARGETS.get(name).targets;
427
+ for (let i = 0; i < targets.length; i += 1) {
428
+ declarations[targets[i]] = value;
429
+ }
430
+ }
431
+ return {
432
+ accepted,
433
+ declarations,
434
+ rejections
435
+ };
436
+ }
437
+ function normalizeForcedTheme(raw) {
438
+ if (raw === "light" || raw === "dark") return raw;
439
+ return null;
440
+ }
441
+
1
442
  // src/loader.ts
2
- var LOADER_VERSION = "3";
443
+ var LOADER_VERSION = "4";
3
444
  (function() {
4
445
  "use strict";
5
446
  if (typeof window === "undefined") return;
@@ -60,6 +501,15 @@ var LOADER_VERSION = "3";
60
501
  function buildCheckoutUrl(internal) {
61
502
  const url = new URL("/customer-pay", internal.origin);
62
503
  url.searchParams.set("merchant", internal.merchant);
504
+ if (internal.theme !== null) {
505
+ url.searchParams.set(THEME_PARAM, internal.theme);
506
+ }
507
+ for (let i = 0; i < CANOPY_THEME_VARIABLES.length; i += 1) {
508
+ const name = CANOPY_THEME_VARIABLES[i];
509
+ const value = internal.themeVariables[name];
510
+ if (value === void 0) continue;
511
+ url.searchParams.set(THEME_VARIABLE_PARAM_PREFIX + name, value);
512
+ }
63
513
  url.hash = "cap=" + encodeURIComponent(internal.capability || "");
64
514
  return url.toString();
65
515
  }
@@ -70,6 +520,15 @@ var LOADER_VERSION = "3";
70
520
  } catch {
71
521
  }
72
522
  }
523
+ function emitThemeRejections(internal, rejections) {
524
+ for (let i = 0; i < rejections.length; i += 1) {
525
+ const rejection = rejections[i];
526
+ emit(internal, "error", {
527
+ code: rejection.code,
528
+ variable: rejection.key
529
+ });
530
+ }
531
+ }
73
532
  function teardownFrame(internal) {
74
533
  if (internal.handshakeTimer) {
75
534
  clearTimeout(internal.handshakeTimer);
@@ -242,6 +701,8 @@ var LOADER_VERSION = "3";
242
701
  }, destroy: function() {
243
702
  } };
244
703
  }
704
+ const forcedTheme = normalizeForcedTheme(opts.theme);
705
+ const themeVariables = resolveThemeVariables(opts.variables, forcedTheme);
245
706
  const node = typeof opts.target === "string" ? document.querySelector(opts.target) : opts.target;
246
707
  if (!node) {
247
708
  return { openNow: function() {
@@ -255,6 +716,8 @@ var LOADER_VERSION = "3";
255
716
  capability: opts.capability,
256
717
  merchant: opts.merchant,
257
718
  origin: originResolution.origin,
719
+ theme: forcedTheme,
720
+ themeVariables: themeVariables.accepted,
258
721
  surface: opts.surface || "popup",
259
722
  handshakeTimeoutMs: opts.handshakeTimeoutMs || DEFAULT_HANDSHAKE_TIMEOUT_MS,
260
723
  fallbackLabel: opts.fallbackLabel || DEFAULT_FALLBACK_LABEL,
@@ -275,6 +738,7 @@ var LOADER_VERSION = "3";
275
738
  handleMessage(internal, event);
276
739
  };
277
740
  window.addEventListener("message", internal.messageListener);
741
+ emitThemeRejections(internal, themeVariables.rejections);
278
742
  if (internal.surface === "iframe" || internal.surface === "auto") {
279
743
  createFrame(internal);
280
744
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@canopypay/checkout-sdk",
3
3
  "private": false,
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
@@ -30,7 +30,7 @@
30
30
  "prepublishOnly": "npm --prefix ../.. run sdk:build && npm run build:npm"
31
31
  },
32
32
  "canopy": {
33
- "loaderVersion": "3",
33
+ "loaderVersion": "4",
34
34
  "apiVersion": "2026-09-01"
35
35
  },
36
36
  "publishConfig": {