@canopypay/checkout-sdk 0.3.0 → 0.4.1

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 B="theme";var T=["accent","background","surface","text","muted","border","radius","font"],U=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"]}]]),q=/^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/,Z=/^(rgb|rgba|hsl|hsla)\(([0-9a-z.,%/ +-]*)\)$/,J=/^[+-]?(?:[0-9]{1,4}(?:\.[0-9]{1,4})?|\.[0-9]{1,4})$/,Q=/^[+-]?(?:[0-9]{1,3}(?:\.[0-9]{1,4})?|\.[0-9]{1,4})%$/,ee=/^(?:0|(?:[0-9]{1,4}(?:\.[0-9]{1,4})?|\.[0-9]{1,4})(?:px|rem|em|%))$/,ne=/^[A-Za-z0-9 ,_-]+$/,te=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}]]),re=new Set(["transparent","currentcolor","inherit","initial","unset","revert","revert-layer"]),oe=new Set(["text","muted","accent"]);function y(t,n,o){return t<n?n:t>o?o:t}function x(t){if(!J.test(t))return null;let n=Number(t);return Number.isFinite(n)?n:null}function v(t){if(!Q.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 ae(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 ie(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 le(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 se(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 ue(t){let n=Z.exec(t);if(n===null)return null;let o=n[1],r=se(n[2]);if(r===null)return null;let u=1;if(r.length===4){let c=ae(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=ie(r[0]),s=v(r[1]),l=v(r[2]);return d===null||s===null||l===null?null:le(d,y(s,0,1),y(l,0,1),u)}function ce(t){if(!q.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=ce(t);if(n!==null)return n;let o=ue(t);if(o!==null)return o;let r=te.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 N(t){return .2126*O(t.r)+.7152*O(t.g)+.0722*O(t.b)}function L(t,n){let o=N(t),r=N(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 H=3,de={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}]},S={r:255,g:255,b:255,a:1};function fe(t,n){let o=n===null?["light","dark"]:[n],r=[];for(let l=0;l<o.length;l+=1){let c=de[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 pe(t,n,o){let r=fe(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<H&&(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,S);else for(let c=0;c<r.length;c+=1){let f=L(w(s,r[c]),S);f<l&&(l=f)}l<H&&(delete t.accent,n.push({key:"accent",code:"variable_contrast_too_low"}))}}function ge(t,n){return t==="colour"?R(n)!==null:t==="length"?ee.test(n):ne.test(n)}function be(t,n){let o=U.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"&&oe.has(t)&&re.has(n.toLowerCase())?{code:"variable_value_erases_content",value:""}:ge(o.kind,n)?{code:null,value:n}:{code:"variable_value_malformed",value:""}}function j(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=be(l,t[l]);if(c.code!==null){r.push({key:l,code:c.code});continue}o[l]=c.value}pe(o,r,n);let d={};for(let s of T){let l=o[s];if(l===void 0)continue;let c=U.get(s).targets;for(let f=0;f<c.length;f+=1)d[c[f]]=l}return{accepted:o,declarations:d,rejections:r}}function z(t){return t==="light"||t==="dark"?t:null}var W="5";(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=46,s="canopy",l="cnpy_pk_live_";function c(){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 f(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 h(e){if(e!==void 0)return f(e);let a=c();return a?{ok:!0,origin:a}:{ok:!1,code:"canopy_origin_undiscoverable"}}function I(e){let a=new URL("/customer-pay",e.origin);a.searchParams.set("merchant",e.merchant),e.theme!==null&&a.searchParams.set(B,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 K(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 M(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 G(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 V(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=I(e)}catch(i){m(e,"error",{code:"popup_navigation_failed"});return}e.popupWin=a,G(e)}}function X(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);V(e,p)}),e.node.appendChild(a),e.fallbackButton=a,m(e,"fallback_offered",{surface:"popup",reason:"iframe_unavailable"})}function D(e){let a=document.createElement("iframe");a.src=I(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"}),M(e),e.surface==="auto"?X(e):m(e,"error",{code:"iframe_blocked"})},e.handshakeTimeoutMs)}function $(e,a){if(a.origin!==e.origin)return;let i=a.data;if(!(!i||i.source!==s)){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){if(!(i.height>=d))return;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 Y(e){if(typeof e.merchant!="string"||e.merchant.indexOf(l)!==0){try{(e.onEvent||function(){})({type:"error",code:"invalid_merchant_key"})}catch(_){}return{openNow:function(){},destroy:function(){}}}let a=h(e.canopyOrigin);if(!a.ok){try{(e.onEvent||function(){})({type:"error",code:a.code})}catch(_){}return{openNow:function(){},destroy:function(){}}}let i=z(e.theme),p=j(e.variables,i),b=typeof e.target=="string"?document.querySelector(e.target):e.target;if(!b)return{openNow:function(){},destroy:function(){}};let P=E.get(b);if(P)return P;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(_){$(g,_)},window.addEventListener("message",g.messageListener),K(g,p.rejections),(g.surface==="iframe"||g.surface==="auto")&&D(g);let F={openNow:function(_){V(g,_)},destroy:function(){g.destroyed||(g.destroyed=!0,window.removeEventListener("message",g.messageListener),M(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,F),F}let k=window;k.Canopy&&k.Canopy.__version===W||(k.Canopy={mount:Y,__version:W})})();})();
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 = "5";
30
471
  (function() {
31
472
  "use strict";
32
473
  if (typeof window === "undefined") return;
@@ -35,6 +476,7 @@ var LOADER_VERSION = "3";
35
476
  const DEFAULT_FALLBACK_LABEL = "Pay with Canopy";
36
477
  const POPUP_POLL_INTERVAL_MS = 500;
37
478
  const INITIAL_FRAME_HEIGHT_PX = 420;
479
+ const MIN_FRAME_HEIGHT_PX = 46;
38
480
  const MESSAGE_SOURCE = "canopy";
39
481
  const PUBLISHABLE_KEY_PREFIX = "cnpy_pk_live_";
40
482
  function resolveOrigin() {
@@ -87,6 +529,15 @@ var LOADER_VERSION = "3";
87
529
  function buildCheckoutUrl(internal) {
88
530
  const url = new URL("/customer-pay", internal.origin);
89
531
  url.searchParams.set("merchant", internal.merchant);
532
+ if (internal.theme !== null) {
533
+ url.searchParams.set(THEME_PARAM, internal.theme);
534
+ }
535
+ for (let i = 0; i < CANOPY_THEME_VARIABLES.length; i += 1) {
536
+ const name = CANOPY_THEME_VARIABLES[i];
537
+ const value = internal.themeVariables[name];
538
+ if (value === void 0) continue;
539
+ url.searchParams.set(THEME_VARIABLE_PARAM_PREFIX + name, value);
540
+ }
90
541
  url.hash = "cap=" + encodeURIComponent(internal.capability || "");
91
542
  return url.toString();
92
543
  }
@@ -97,6 +548,15 @@ var LOADER_VERSION = "3";
97
548
  } catch {
98
549
  }
99
550
  }
551
+ function emitThemeRejections(internal, rejections) {
552
+ for (let i = 0; i < rejections.length; i += 1) {
553
+ const rejection = rejections[i];
554
+ emit(internal, "error", {
555
+ code: rejection.code,
556
+ variable: rejection.key
557
+ });
558
+ }
559
+ }
100
560
  function teardownFrame(internal) {
101
561
  if (internal.handshakeTimer) {
102
562
  clearTimeout(internal.handshakeTimer);
@@ -228,6 +688,7 @@ var LOADER_VERSION = "3";
228
688
  return;
229
689
  }
230
690
  if (data.type === "resize" && typeof data.height === "number" && internal.frame) {
691
+ if (!(data.height >= MIN_FRAME_HEIGHT_PX)) return;
231
692
  internal.frame.style.height = data.height + "px";
232
693
  return;
233
694
  }
@@ -269,6 +730,8 @@ var LOADER_VERSION = "3";
269
730
  }, destroy: function() {
270
731
  } };
271
732
  }
733
+ const forcedTheme = normalizeForcedTheme(opts.theme);
734
+ const themeVariables = resolveThemeVariables(opts.variables, forcedTheme);
272
735
  const node = typeof opts.target === "string" ? document.querySelector(opts.target) : opts.target;
273
736
  if (!node) {
274
737
  return { openNow: function() {
@@ -282,6 +745,8 @@ var LOADER_VERSION = "3";
282
745
  capability: opts.capability,
283
746
  merchant: opts.merchant,
284
747
  origin: originResolution.origin,
748
+ theme: forcedTheme,
749
+ themeVariables: themeVariables.accepted,
285
750
  surface: opts.surface || "popup",
286
751
  handshakeTimeoutMs: opts.handshakeTimeoutMs || DEFAULT_HANDSHAKE_TIMEOUT_MS,
287
752
  fallbackLabel: opts.fallbackLabel || DEFAULT_FALLBACK_LABEL,
@@ -302,6 +767,7 @@ var LOADER_VERSION = "3";
302
767
  handleMessage(internal, event);
303
768
  };
304
769
  window.addEventListener("message", internal.messageListener);
770
+ emitThemeRejections(internal, themeVariables.rejections);
305
771
  if (internal.surface === "iframe" || internal.surface === "auto") {
306
772
  createFrame(internal);
307
773
  }
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 = "5";
3
444
  (function() {
4
445
  "use strict";
5
446
  if (typeof window === "undefined") return;
@@ -8,6 +449,7 @@ var LOADER_VERSION = "3";
8
449
  const DEFAULT_FALLBACK_LABEL = "Pay with Canopy";
9
450
  const POPUP_POLL_INTERVAL_MS = 500;
10
451
  const INITIAL_FRAME_HEIGHT_PX = 420;
452
+ const MIN_FRAME_HEIGHT_PX = 46;
11
453
  const MESSAGE_SOURCE = "canopy";
12
454
  const PUBLISHABLE_KEY_PREFIX = "cnpy_pk_live_";
13
455
  function resolveOrigin() {
@@ -60,6 +502,15 @@ var LOADER_VERSION = "3";
60
502
  function buildCheckoutUrl(internal) {
61
503
  const url = new URL("/customer-pay", internal.origin);
62
504
  url.searchParams.set("merchant", internal.merchant);
505
+ if (internal.theme !== null) {
506
+ url.searchParams.set(THEME_PARAM, internal.theme);
507
+ }
508
+ for (let i = 0; i < CANOPY_THEME_VARIABLES.length; i += 1) {
509
+ const name = CANOPY_THEME_VARIABLES[i];
510
+ const value = internal.themeVariables[name];
511
+ if (value === void 0) continue;
512
+ url.searchParams.set(THEME_VARIABLE_PARAM_PREFIX + name, value);
513
+ }
63
514
  url.hash = "cap=" + encodeURIComponent(internal.capability || "");
64
515
  return url.toString();
65
516
  }
@@ -70,6 +521,15 @@ var LOADER_VERSION = "3";
70
521
  } catch {
71
522
  }
72
523
  }
524
+ function emitThemeRejections(internal, rejections) {
525
+ for (let i = 0; i < rejections.length; i += 1) {
526
+ const rejection = rejections[i];
527
+ emit(internal, "error", {
528
+ code: rejection.code,
529
+ variable: rejection.key
530
+ });
531
+ }
532
+ }
73
533
  function teardownFrame(internal) {
74
534
  if (internal.handshakeTimer) {
75
535
  clearTimeout(internal.handshakeTimer);
@@ -201,6 +661,7 @@ var LOADER_VERSION = "3";
201
661
  return;
202
662
  }
203
663
  if (data.type === "resize" && typeof data.height === "number" && internal.frame) {
664
+ if (!(data.height >= MIN_FRAME_HEIGHT_PX)) return;
204
665
  internal.frame.style.height = data.height + "px";
205
666
  return;
206
667
  }
@@ -242,6 +703,8 @@ var LOADER_VERSION = "3";
242
703
  }, destroy: function() {
243
704
  } };
244
705
  }
706
+ const forcedTheme = normalizeForcedTheme(opts.theme);
707
+ const themeVariables = resolveThemeVariables(opts.variables, forcedTheme);
245
708
  const node = typeof opts.target === "string" ? document.querySelector(opts.target) : opts.target;
246
709
  if (!node) {
247
710
  return { openNow: function() {
@@ -255,6 +718,8 @@ var LOADER_VERSION = "3";
255
718
  capability: opts.capability,
256
719
  merchant: opts.merchant,
257
720
  origin: originResolution.origin,
721
+ theme: forcedTheme,
722
+ themeVariables: themeVariables.accepted,
258
723
  surface: opts.surface || "popup",
259
724
  handshakeTimeoutMs: opts.handshakeTimeoutMs || DEFAULT_HANDSHAKE_TIMEOUT_MS,
260
725
  fallbackLabel: opts.fallbackLabel || DEFAULT_FALLBACK_LABEL,
@@ -275,6 +740,7 @@ var LOADER_VERSION = "3";
275
740
  handleMessage(internal, event);
276
741
  };
277
742
  window.addEventListener("message", internal.messageListener);
743
+ emitThemeRejections(internal, themeVariables.rejections);
278
744
  if (internal.surface === "iframe" || internal.surface === "auto") {
279
745
  createFrame(internal);
280
746
  }
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.1",
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": "5",
34
34
  "apiVersion": "2026-09-01"
35
35
  },
36
36
  "publishConfig": {