@canopypay/checkout-sdk 0.2.1 → 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",m=2500,y="Pay with Canopy",h=500,g="canopy",v="cnpy_pk_live_";function b(){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 k(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){if(e!==void 0)return k(e);let n=b();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 E(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,E(e)}}function T(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 w(e){let n=document.createElement("iframe");n.src=c(e),n.style.width="100%",n.style.border="none",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"?T(e):i(e,"error",{code:"iframe_blocked"})},e.handshakeTimeoutMs)}function O(e,n){if(n.origin!==e.origin)return;let o=n.data;if(!(!o||o.source!==g)){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 L(e){if(typeof e.merchant!="string"||e.merchant.indexOf(v)!==0){try{(e.onEvent||function(){})({type:"error",code:"invalid_merchant_key"})}catch(a){}return{openNow:function(){},destroy:function(){}}}let n=_(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||m,fallbackLabel:e.fallbackLabel||y,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){O(t,a)},window.addEventListener("message",t.messageListener),(t.surface==="iframe"||t.surface==="auto")&&w(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==="2"||(u.Canopy={mount:L,__version:"2"})})();})();
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 = "2";
470
+ var LOADER_VERSION = "4";
30
471
  (function() {
31
472
  "use strict";
32
473
  if (typeof window === "undefined") return;
@@ -34,6 +475,7 @@ var LOADER_VERSION = "2";
34
475
  const DEFAULT_HANDSHAKE_TIMEOUT_MS = 2500;
35
476
  const DEFAULT_FALLBACK_LABEL = "Pay with Canopy";
36
477
  const POPUP_POLL_INTERVAL_MS = 500;
478
+ const INITIAL_FRAME_HEIGHT_PX = 420;
37
479
  const MESSAGE_SOURCE = "canopy";
38
480
  const PUBLISHABLE_KEY_PREFIX = "cnpy_pk_live_";
39
481
  function resolveOrigin() {
@@ -86,6 +528,15 @@ var LOADER_VERSION = "2";
86
528
  function buildCheckoutUrl(internal) {
87
529
  const url = new URL("/customer-pay", internal.origin);
88
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
+ }
89
540
  url.hash = "cap=" + encodeURIComponent(internal.capability || "");
90
541
  return url.toString();
91
542
  }
@@ -96,6 +547,15 @@ var LOADER_VERSION = "2";
96
547
  } catch {
97
548
  }
98
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
+ }
99
559
  function teardownFrame(internal) {
100
560
  if (internal.handshakeTimer) {
101
561
  clearTimeout(internal.handshakeTimer);
@@ -186,6 +646,9 @@ var LOADER_VERSION = "2";
186
646
  iframe.src = buildCheckoutUrl(internal);
187
647
  iframe.style.width = "100%";
188
648
  iframe.style.border = "none";
649
+ iframe.setAttribute("allow", "clipboard-write");
650
+ iframe.setAttribute("title", "Canopy checkout");
651
+ iframe.style.height = INITIAL_FRAME_HEIGHT_PX + "px";
189
652
  internal.node.appendChild(iframe);
190
653
  internal.frame = iframe;
191
654
  if (typeof ResizeObserver !== "undefined") {
@@ -265,6 +728,8 @@ var LOADER_VERSION = "2";
265
728
  }, destroy: function() {
266
729
  } };
267
730
  }
731
+ const forcedTheme = normalizeForcedTheme(opts.theme);
732
+ const themeVariables = resolveThemeVariables(opts.variables, forcedTheme);
268
733
  const node = typeof opts.target === "string" ? document.querySelector(opts.target) : opts.target;
269
734
  if (!node) {
270
735
  return { openNow: function() {
@@ -278,6 +743,8 @@ var LOADER_VERSION = "2";
278
743
  capability: opts.capability,
279
744
  merchant: opts.merchant,
280
745
  origin: originResolution.origin,
746
+ theme: forcedTheme,
747
+ themeVariables: themeVariables.accepted,
281
748
  surface: opts.surface || "popup",
282
749
  handshakeTimeoutMs: opts.handshakeTimeoutMs || DEFAULT_HANDSHAKE_TIMEOUT_MS,
283
750
  fallbackLabel: opts.fallbackLabel || DEFAULT_FALLBACK_LABEL,
@@ -298,6 +765,7 @@ var LOADER_VERSION = "2";
298
765
  handleMessage(internal, event);
299
766
  };
300
767
  window.addEventListener("message", internal.messageListener);
768
+ emitThemeRejections(internal, themeVariables.rejections);
301
769
  if (internal.surface === "iframe" || internal.surface === "auto") {
302
770
  createFrame(internal);
303
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,16 @@
31
66
  * | "auto"`, default `"popup"`), seven events (`ready`, `resize`, `paid`,
32
67
  * `error`, `close`, `surface_unavailable`, `fallback_offered`).
33
68
  */
69
+
70
+ /**
71
+ * `close` is POPUP-ONLY. It reports that the payer closed the checkout
72
+ * window, which the loader detects by polling `popupWin.closed`. An inline
73
+ * (`surface: "iframe"`) embed has nothing to close -- the merchant owns the
74
+ * container and removes it themselves -- so no `close` is ever emitted
75
+ * there, and none is synthesised to make the two surfaces look alike.
76
+ *
77
+ * Every other event reaches both surfaces.
78
+ */
34
79
  type CanopyEventType = "ready" | "resize" | "paid" | "error" | "close" | "surface_unavailable" | "fallback_offered";
35
80
  type CanopyEvent = {
36
81
  type: CanopyEventType;
@@ -59,6 +104,28 @@ type MountOptions = {
59
104
  * script tag when both are available.
60
105
  */
61
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;
62
129
  };
63
130
  type CanopyHandle = {
64
131
  openNow: (win: Window | null) => void;
@@ -88,4 +155,4 @@ declare function mount(opts: MountOptions): CanopyHandle;
88
155
  * `undefined` server-side / before the loader has installed the global. */
89
156
  declare function version(): string | undefined;
90
157
 
91
- 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,16 @@
31
66
  * | "auto"`, default `"popup"`), seven events (`ready`, `resize`, `paid`,
32
67
  * `error`, `close`, `surface_unavailable`, `fallback_offered`).
33
68
  */
69
+
70
+ /**
71
+ * `close` is POPUP-ONLY. It reports that the payer closed the checkout
72
+ * window, which the loader detects by polling `popupWin.closed`. An inline
73
+ * (`surface: "iframe"`) embed has nothing to close -- the merchant owns the
74
+ * container and removes it themselves -- so no `close` is ever emitted
75
+ * there, and none is synthesised to make the two surfaces look alike.
76
+ *
77
+ * Every other event reaches both surfaces.
78
+ */
34
79
  type CanopyEventType = "ready" | "resize" | "paid" | "error" | "close" | "surface_unavailable" | "fallback_offered";
35
80
  type CanopyEvent = {
36
81
  type: CanopyEventType;
@@ -59,6 +104,28 @@ type MountOptions = {
59
104
  * script tag when both are available.
60
105
  */
61
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;
62
129
  };
63
130
  type CanopyHandle = {
64
131
  openNow: (win: Window | null) => void;
@@ -88,4 +155,4 @@ declare function mount(opts: MountOptions): CanopyHandle;
88
155
  * `undefined` server-side / before the loader has installed the global. */
89
156
  declare function version(): string | undefined;
90
157
 
91
- 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 = "2";
443
+ var LOADER_VERSION = "4";
3
444
  (function() {
4
445
  "use strict";
5
446
  if (typeof window === "undefined") return;
@@ -7,6 +448,7 @@ var LOADER_VERSION = "2";
7
448
  const DEFAULT_HANDSHAKE_TIMEOUT_MS = 2500;
8
449
  const DEFAULT_FALLBACK_LABEL = "Pay with Canopy";
9
450
  const POPUP_POLL_INTERVAL_MS = 500;
451
+ const INITIAL_FRAME_HEIGHT_PX = 420;
10
452
  const MESSAGE_SOURCE = "canopy";
11
453
  const PUBLISHABLE_KEY_PREFIX = "cnpy_pk_live_";
12
454
  function resolveOrigin() {
@@ -59,6 +501,15 @@ var LOADER_VERSION = "2";
59
501
  function buildCheckoutUrl(internal) {
60
502
  const url = new URL("/customer-pay", internal.origin);
61
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
+ }
62
513
  url.hash = "cap=" + encodeURIComponent(internal.capability || "");
63
514
  return url.toString();
64
515
  }
@@ -69,6 +520,15 @@ var LOADER_VERSION = "2";
69
520
  } catch {
70
521
  }
71
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
+ }
72
532
  function teardownFrame(internal) {
73
533
  if (internal.handshakeTimer) {
74
534
  clearTimeout(internal.handshakeTimer);
@@ -159,6 +619,9 @@ var LOADER_VERSION = "2";
159
619
  iframe.src = buildCheckoutUrl(internal);
160
620
  iframe.style.width = "100%";
161
621
  iframe.style.border = "none";
622
+ iframe.setAttribute("allow", "clipboard-write");
623
+ iframe.setAttribute("title", "Canopy checkout");
624
+ iframe.style.height = INITIAL_FRAME_HEIGHT_PX + "px";
162
625
  internal.node.appendChild(iframe);
163
626
  internal.frame = iframe;
164
627
  if (typeof ResizeObserver !== "undefined") {
@@ -238,6 +701,8 @@ var LOADER_VERSION = "2";
238
701
  }, destroy: function() {
239
702
  } };
240
703
  }
704
+ const forcedTheme = normalizeForcedTheme(opts.theme);
705
+ const themeVariables = resolveThemeVariables(opts.variables, forcedTheme);
241
706
  const node = typeof opts.target === "string" ? document.querySelector(opts.target) : opts.target;
242
707
  if (!node) {
243
708
  return { openNow: function() {
@@ -251,6 +716,8 @@ var LOADER_VERSION = "2";
251
716
  capability: opts.capability,
252
717
  merchant: opts.merchant,
253
718
  origin: originResolution.origin,
719
+ theme: forcedTheme,
720
+ themeVariables: themeVariables.accepted,
254
721
  surface: opts.surface || "popup",
255
722
  handshakeTimeoutMs: opts.handshakeTimeoutMs || DEFAULT_HANDSHAKE_TIMEOUT_MS,
256
723
  fallbackLabel: opts.fallbackLabel || DEFAULT_FALLBACK_LABEL,
@@ -271,6 +738,7 @@ var LOADER_VERSION = "2";
271
738
  handleMessage(internal, event);
272
739
  };
273
740
  window.addEventListener("message", internal.messageListener);
741
+ emitThemeRejections(internal, themeVariables.rejections);
274
742
  if (internal.surface === "iframe" || internal.surface === "auto") {
275
743
  createFrame(internal);
276
744
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@canopypay/checkout-sdk",
3
3
  "private": false,
4
- "version": "0.2.1",
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": "2",
33
+ "loaderVersion": "4",
34
34
  "apiVersion": "2026-09-01"
35
35
  },
36
36
  "publishConfig": {