@chaindaddy/js 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +5335 -0
- package/dist/api.js +0 -0
- package/dist/cd.js +14 -2
- package/dist/cd.js.sri +1 -1
- package/dist/checkout.d.ts +73 -0
- package/dist/checkout.js +168 -0
- package/dist/embed.d.ts +8 -0
- package/dist/embed.js +188 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +345 -1
- package/dist/verify.d.ts +126 -0
- package/dist/verify.js +342 -0
- package/package.json +19 -2
package/dist/api.js
ADDED
|
File without changes
|
package/dist/cd.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";(()=>{var
|
|
1
|
+
"use strict";(()=>{var S="0.3.0";function v(t){let e="";for(let n of t)e+=String.fromCharCode(n);return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function f(){return v(crypto.getRandomValues(new Uint8Array(32)))}function R(t,e,n,s){let a=o=>{if(o&&typeof o=="object"&&o.type===t)try{n(o)}catch{}},d=o=>{o.origin===e&&a(o.data)};window.addEventListener("message",d);let u=null;if(s&&typeof BroadcastChannel=="function"&&e===location.origin)try{u=new BroadcastChannel(s),u.onmessage=o=>a(o.data)}catch{u=null}return()=>{window.removeEventListener("message",d),u?.close()}}var m={get(t){try{return sessionStorage.getItem(t)}catch{return null}},set(t,e){try{sessionStorage.setItem(t,e)}catch{}},remove(t){try{sessionStorage.removeItem(t)}catch{}}};var q="https://api.chaindaddy.io",V={"https://api.chaindaddy.io":"https://chaindaddy.io/_oauth/authorize","https://staging-api.chaindaddy.io":"https://staging.chaindaddy.io/_oauth/authorize"},w="cd-signin",T="cd:signin",y="cd.signin";async function F(t){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t));return v(new Uint8Array(e))}function G(t="openid"){let e=t.split(/\s+/).filter(Boolean);return(e.includes("openid")?e:["openid",...e]).join(" ")}async function K(t,e,n){let s=new URLSearchParams({response_type:"code",client_id:e.clientId,redirect_uri:e.redirectUri,scope:G(t.scope),state:e.state,nonce:e.nonce,code_challenge:await F(e.verifier),code_challenge_method:"S256"});return t.claims&&s.set("claims",typeof t.claims=="string"?t.claims:JSON.stringify(t.claims)),`${n}?${s}`}function _(t,e){return{code:e,codeVerifier:t.verifier,state:t.state,nonce:t.nonce,issuer:t.issuer,clientId:t.clientId,redirectUri:t.redirectUri}}function b(t){let e=(t.issuer??q).replace(/\/$/,""),n=t.authorizeUrl??V[e];if(!n)return Promise.reject(new Error("authorizeUrl is required for this issuer"));if(!t.clientId||!t.redirectUri)return Promise.reject(new Error("clientId and redirectUri are required"));let s={state:f(),nonce:f(),verifier:f(),issuer:e,clientId:t.clientId,redirectUri:t.redirectUri};m.set(y,JSON.stringify(s));let a=window.open("",w,"popup,width=480,height=720");return(async()=>{let d=await K(t,s,n);if(!a)return location.assign(d),null;a.location.href=d;let u=new URL(t.redirectUri,location.href).origin;return new Promise((o,i)=>{let c=R(T,u,r=>{r.state===s.state&&(c(),m.remove(y),typeof r.error=="string"&&r.error?i(new Error(r.error)):r.iss!==e?i(new Error("the answer came from another issuer")):typeof r.code!="string"||!r.code?i(new Error("the answer carried no code")):o(_(s,r.code)))},w);t.signal?.addEventListener("abort",()=>{c(),i(new Error("aborted"))})})})()}async function U(){let t=new URLSearchParams(location.search),e={type:T,code:t.get("code"),state:t.get("state"),iss:t.get("iss"),error:t.get("error")};if(window.name===w){try{window.opener?.postMessage(e,location.origin)}catch{}try{let a=new BroadcastChannel(w);a.postMessage(e),a.close()}catch{}return window.close(),null}let n=m.get(y);m.remove(y);let s=n?JSON.parse(n):null;if(!s||e.state!==s.state)throw new Error("the sign-in state does not match: start again");if(e.error)throw new Error(e.error);if(e.iss!==s.issuer)throw new Error("the answer came from another issuer");if(!e.code)throw new Error("the answer carried no code");return _(s,e.code)}async function A(t){let e=await fetch(`${t.issuer}/api/v2/oauth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:t.code,redirect_uri:t.redirectUri,client_id:t.clientId,code_verifier:t.codeVerifier})}),n=await e.json();if(!e.ok)throw new Error(`${n.error??e.status}: ${n.error_description??""}`.trim());return n}var W='<svg viewBox="0 0 24 24" fill="#FFD700" width="20" height="20" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><path d="M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5z" stroke="#B8860B" stroke-width="0.5"/><path d="M5 18h14v2H5v-2z" stroke="#B8860B" stroke-width="0.5"/></svg>',Y=`
|
|
2
2
|
:host { display: inline-block; }
|
|
3
3
|
button { display: inline-flex; align-items: center; justify-content: center; gap: 10px; box-sizing: border-box;
|
|
4
4
|
min-height: 40px; padding: 0 16px; border-radius: 10px; font: 600 15px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
@@ -7,4 +7,16 @@ button[data-theme="dark"] { background: #101828; color: #ffffff; border-color: #
|
|
|
7
7
|
button:focus-visible { outline: 2px solid #f59e0b; outline-offset: 2px; }
|
|
8
8
|
button:disabled { opacity: .6; cursor: default; }
|
|
9
9
|
svg { flex: none; }
|
|
10
|
-
`;function
|
|
10
|
+
`;function L(){if(typeof customElements>"u"||customElements.get("cd-signin-button"))return;class t extends HTMLElement{constructor(){super(),this.root=this.attachShadow({mode:"open"})}connectedCallback(){this.render()}attributeChangedCallback(){this.isConnected&&this.render()}render(){let n=this.getAttribute("text")==="continue"?"Continue with Chain Daddy":"Sign in with Chain Daddy",s=this.getAttribute("theme")==="dark"?"dark":"light";this.root.innerHTML=`<style>${Y}</style><button type="button" part="button" data-theme="${s}">${W}<span></span></button>`;let a=this.root.querySelector("button");a.querySelector("span").textContent=n,a.addEventListener("click",()=>this.start())}start(){let n=s=>this.getAttribute(s)||void 0;b({clientId:n("client-id")??"",redirectUri:n("redirect-uri")??"",scope:n("scope"),claims:n("claims"),issuer:n("issuer"),authorizeUrl:n("authorize-url")}).then(s=>s&&this.dispatchEvent(new CustomEvent("cd-signin",{detail:s,bubbles:!0,composed:!0})),s=>this.dispatchEvent(new CustomEvent("cd-signin-error",{detail:{message:String(s?.message??s)},bubbles:!0,composed:!0})))}}t.observedAttributes=["theme","text"],customElements.define("cd-signin-button",t)}var M="https://embed.chaindaddy.io",k="https://embed-staging.chaindaddy.io";var J=["--cd-accent","--cd-surface","--cd-text","--cd-text-muted","--cd-border"];function P(t){try{if(t&&new URL(t).hostname==="js.staging.chaindaddy.io")return k}catch{}return M}var X=`
|
|
11
|
+
:host { display: block; }
|
|
12
|
+
iframe { display: block; width: 100%; border: 0; height: 1px; opacity: 0; color-scheme: normal; }
|
|
13
|
+
:host([data-cd-ready]) iframe { opacity: 1; }
|
|
14
|
+
:host([data-cd-ready]) slot { display: none; }
|
|
15
|
+
:host([data-cd-unavailable]) iframe { display: none; }
|
|
16
|
+
`,Q=`
|
|
17
|
+
:host { display: inline-block; }
|
|
18
|
+
button { box-sizing: border-box; min-height: 40px; padding: 0 16px; border-radius: 10px; cursor: pointer;
|
|
19
|
+
font: 600 15px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
20
|
+
border: 1px solid var(--cd-border, #d0d5dd); background: var(--cd-accent, #f59e0b); color: var(--cd-text, #101828); }
|
|
21
|
+
button:focus-visible { outline: 2px solid var(--cd-accent, #f59e0b); outline-offset: 2px; }
|
|
22
|
+
`;function O(t={}){try{if(typeof customElements>"u"||typeof HTMLElement>"u")return;let e=t.origin===k?k:M;class n extends HTMLElement{constructor(){super(...arguments);this.kind="app";this.frame=null;this.onMessage=i=>this.receive(i)}connectedCallback(){try{this.shadowRoot||this.attachShadow({mode:"open"}),this.mount()}catch{this.unavailable()}}disconnectedCallback(){try{window.removeEventListener("message",this.onMessage),clearTimeout(this.timer)}catch{}}attributeChangedCallback(){try{this.isConnected&&this.shadowRoot&&this.mount()}catch{this.unavailable()}}src(){let i=(this.getAttribute("crown")??"").trim(),c=(this.getAttribute("chain")??"").trim(),r=(this.getAttribute("app")??"").trim();if(!/^[0-9]{1,18}$/.test(i)||!c||this.kind==="app"&&!r)return null;let l=new URLSearchParams({crown:i,chain:c});return this.kind==="app"?l.set("app",r):l.set("kind","coin"),l.set("mode",this.mode()),l.set("v","1"),`${e}/v1/frame?${l.toString()}`}mode(){let i=this.getAttribute("mode");if(i==="light"||i==="dark")return i;try{return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}catch{return"light"}}mount(){let i=this.shadowRoot;window.removeEventListener("message",this.onMessage),clearTimeout(this.timer),this.removeAttribute("data-cd-ready"),this.removeAttribute("data-cd-unavailable");let c=this.src();if(!c){i.innerHTML="<slot></slot>",this.frame=null;return}i.innerHTML=`<style>${X}</style><slot></slot>`;let r=document.createElement("iframe");r.setAttribute("loading","lazy"),r.setAttribute("referrerpolicy","strict-origin-when-cross-origin"),r.setAttribute("allow","clipboard-write; fullscreen"),r.setAttribute("title",this.kind==="coin"?"Coin on Chain Daddy":"App on Chain Daddy"),r.src=c,i.appendChild(r),this.frame=r,window.addEventListener("message",this.onMessage),this.timer=setTimeout(()=>{this.hasAttribute("data-cd-ready")||this.unavailable()},8e3)}receive(i){try{if(i.origin!==e||!this.frame||!i.source||i.source!==this.frame.contentWindow)return;let c=i.data;if(!c||typeof c!="object")return;if(c.type==="cd:ready")clearTimeout(this.timer),this.removeAttribute("data-cd-unavailable"),this.setAttribute("data-cd-ready",""),this.sendTheme(),this.dispatchEvent(new CustomEvent("cd:ready"));else if(c.type==="cd:resize"){let r=Number(c.height);Number.isFinite(r)&&(this.frame.style.height=`${Math.max(1,Math.min(4e3,Math.ceil(r)))}px`)}else c.type==="cd:unavailable"&&this.unavailable()}catch{}}sendTheme(){let i={},c=getComputedStyle(this);for(let r of J){let l=c.getPropertyValue(r).trim();l&&(i[r]=l)}this.frame?.contentWindow?.postMessage({type:"cd:theme",tokens:i},e)}unavailable(){try{if(clearTimeout(this.timer),this.removeAttribute("data-cd-ready"),this.hasAttribute("data-cd-unavailable"))return;this.setAttribute("data-cd-unavailable",""),this.dispatchEvent(new CustomEvent("cd:unavailable"))}catch{}}}n.observedAttributes=["crown","chain","app","mode"];class s extends n{}class a extends n{constructor(){super(...arguments);this.kind="coin"}}class d extends HTMLElement{connectedCallback(){try{let o=this.querySelector("a[href]"),i=o?new URL(o.getAttribute("href")??"",document.baseURI):null;if(!i||i.protocol!=="https:")return;let c=this.shadowRoot??this.attachShadow({mode:"open"});c.innerHTML=`<style>${Q}</style><button type="button" part="button"></button>`;let r=c.querySelector("button");r.textContent=(o.textContent??"").trim()||"Buy on Chain Daddy",r.addEventListener("click",()=>{try{window.open(i.toString(),"cd-buy","popup,width=460,height=760")||window.location.assign(i.toString())}catch{}})}catch{}}}for(let[u,o]of[["cd-app",s],["cd-coin",a],["cd-buy",d]])customElements.get(u)||customElements.define(u,o)}catch{}}var Z=["https://chaindaddy.io","https://staging.chaindaddy.io"],D=()=>"cd-checkout-"+f(),H="popup,width=480,height=820",$=300,tt=7;function E(t){if(typeof t!="string")return null;let e;try{e=new URL(t)}catch{return null}let n=/^\/_pay\/([\w-]+)$/.exec(e.pathname);return n&&Z.includes(e.origin)&&!e.username&&!e.password?{href:e.origin+e.pathname,origin:e.origin,id:n[1]}:null}function C(t,e){if(typeof t!="string")return null;let n;try{n=new URL(t,location.href)}catch{return null}if(!/^https?:$/.test(n.protocol)||n.username||n.password)return null;let s=d=>{let u=d.split("=")[0];try{return decodeURIComponent(u.replace(/\+/g," "))}catch{return u}},a=n.search.slice(1).split("&").filter(d=>d&&s(d)!=="session_id");return a.push("session_id="+encodeURIComponent(e)),n.origin+n.pathname+"?"+a.join("&")+n.hash}var p;function N(t,e,n){return new Promise(s=>{let a=!1,d=0,u,o=l=>{clearInterval(r),clearTimeout(u),window.removeEventListener("message",i),n&&l.status==="completed"&&location.assign(n),s(l)},i=l=>{if(l.origin!==e.origin||l.source!==t)return;let g=l.data;if(!g||typeof g!="object")return;g.type==="cd:checkout.ready"&&(a=!0);let h=g.session;if(!(!h||typeof h!="object"||h.id!==e.id)){if(g.type==="cd:checkout.cancelled")o({status:"cancelled",session:{id:e.id}});else if(g.type==="cd:checkout.completed"&&(h.status==="complete"||h.status==="paid")){let I={id:e.id,status:h.status};typeof h.orderId=="string"&&/^[\w-]{1,128}$/.test(h.orderId)&&(I.orderId=h.orderId),o({status:"completed",session:I})}}},c=()=>{try{t.postMessage({type:"cd:checkout.hello"},e.origin)}catch{}},r=setInterval(()=>{t.closed?(clearInterval(r),u=setTimeout(()=>o({status:"closed"}),$)):(!a||++d%tt===0)&&c()},$);window.addEventListener("message",i),c()})}function x(t){let e=()=>new Error("checkout: url is not a Chain Daddy checkout session link"),n=(a,d,u)=>{p={popup:a,result:d,href:u};let o=()=>{p&&p.popup===a&&(p=void 0)};return d.then(o,o),d},s=a=>(location.assign(a.href),{status:"redirected"});try{let{url:a,successUrl:d}=t;if(d&&!C(d,""))throw new Error("checkout: successUrl is not a web address");let u=r=>d?C(d,r.id):null,o=typeof a=="function"?null:E(a);if(typeof a!="function"&&!o)throw e();if(p&&!p.popup.closed&&(!o||p.href===o.href))return p.popup.focus(),p.result;if(o){let r=window.open(o.href,D(),H);return r?n(r,N(r,o,u(o)),o.href):Promise.resolve(s(o))}let i=window.open("",D(),H),c=new Promise(r=>r(a())).then(r=>{let l=E(r);if(!l)throw i?.close(),e();return i?(i.location.href=l.href,N(i,l,u(l))):s(l)},r=>{throw i?.close(),r});return i?n(i,c):c}catch(a){return Promise.reject(a)}}var B=!1,z;function j(){B||typeof document>"u"||(B=!0,document.addEventListener("click",t=>{let e=t.target instanceof Element?t.target.closest("[data-cd-checkout]"):null;if(!e||t.defaultPrevented||t.button||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey)return;let n=e.getAttribute("data-cd-checkout")||e.href,s=e.getAttribute("data-cd-success");if(!E(n)||s&&!C(s,""))return;t.preventDefault();let a=x({url:n,successUrl:s});a!==z&&(z=a,a.then(d=>void e.dispatchEvent(new CustomEvent("cd:checkout",{detail:d,bubbles:!0})),()=>{}))}))}var et={version:S,signIn:b,handleCallback:U,exchangeCode:A,checkout:x};window.ChainDaddy=Object.assign(window.ChainDaddy??{},et);L();O({origin:P(document.currentScript?.src)});j();})();
|
package/dist/cd.js.sri
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
sha384-
|
|
1
|
+
sha384-oAzz7JY6TsxFo7MvOAoI5wWjgXMDQsM27deR2Rr9F1DCHjZMz+Tb0oN9LT46GSnm
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export interface CheckoutOptions {
|
|
2
|
+
/**
|
|
3
|
+
* The session's `url`, or a function that fetches it. With a function the
|
|
4
|
+
* popup opens blank at once (inside the click) and goes to the link when it
|
|
5
|
+
* arrives.
|
|
6
|
+
*/
|
|
7
|
+
url: string | (() => string | Promise<string>);
|
|
8
|
+
/**
|
|
9
|
+
* Where your page goes once the buyer has paid, with `session_id` added, as
|
|
10
|
+
* the checkout's own return does. Left out, the call only resolves. The
|
|
11
|
+
* checkout never tells this page where to go.
|
|
12
|
+
*/
|
|
13
|
+
successUrl?: string | null;
|
|
14
|
+
}
|
|
15
|
+
export type CheckoutResult =
|
|
16
|
+
/** The buyer paid. `session.status` is "complete" (confirmed) or "paid" (sent, still confirming). */
|
|
17
|
+
{
|
|
18
|
+
status: 'completed';
|
|
19
|
+
session: {
|
|
20
|
+
id: string;
|
|
21
|
+
status: 'complete' | 'paid';
|
|
22
|
+
orderId?: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** The buyer left the checkout without paying. */
|
|
26
|
+
| {
|
|
27
|
+
status: 'cancelled';
|
|
28
|
+
session: {
|
|
29
|
+
id: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** The popup closed and said nothing: read the session on your server to know. */
|
|
33
|
+
| {
|
|
34
|
+
status: 'closed';
|
|
35
|
+
}
|
|
36
|
+
/** The popup was blocked, so this page is going to the session by link. */
|
|
37
|
+
| {
|
|
38
|
+
status: 'redirected';
|
|
39
|
+
};
|
|
40
|
+
/** A session page: its address as parsed, its origin, and the session's id. */
|
|
41
|
+
export interface CheckoutTarget {
|
|
42
|
+
href: string;
|
|
43
|
+
origin: string;
|
|
44
|
+
id: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The session page `url` names, or null when it is anything else. Exact
|
|
48
|
+
* origin (so https, our host, no port), no credentials, and the path
|
|
49
|
+
* /_pay/<id> and nothing under it, all read from the parsed URL: never a
|
|
50
|
+
* prefix test on the string, which "https://chaindaddy.io@evil.example/_pay/x"
|
|
51
|
+
* passes. `href` is the page and nothing more: a query or fragment on the
|
|
52
|
+
* link is dropped, so nothing rides into our site on a checkout link.
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseCheckoutUrl(url: unknown): CheckoutTarget | null;
|
|
55
|
+
/**
|
|
56
|
+
* Opens a checkout session in a popup. Call it from a click handler: the popup
|
|
57
|
+
* opens at once, or a browser blocks it. Resolves with how it ended; rejects,
|
|
58
|
+
* before any window opens or moves, when `url` is not a checkout session link.
|
|
59
|
+
* When the popup is blocked, the page goes to the session by link instead.
|
|
60
|
+
*/
|
|
61
|
+
export declare function checkout(o: CheckoutOptions): Promise<CheckoutResult>;
|
|
62
|
+
/**
|
|
63
|
+
* Makes every element with `data-cd-checkout` open its session in the popup:
|
|
64
|
+
* a link by its `href`, anything else by the attribute's value. One listener
|
|
65
|
+
* on the document, so elements added later work too. On a link this only adds
|
|
66
|
+
* to what the link already does: without this script, with a modifier key, or
|
|
67
|
+
* when the address is not a session link, the link is followed as usual.
|
|
68
|
+
*
|
|
69
|
+
* When the visit ends the element fires `cd:checkout` (bubbling) with the
|
|
70
|
+
* result as `detail`; once the buyer has paid, the page goes to the element's
|
|
71
|
+
* `data-cd-success`, if it has one.
|
|
72
|
+
*/
|
|
73
|
+
export declare function bindCheckoutButtons(): void;
|
package/dist/checkout.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// src/core.ts
|
|
2
|
+
function b64url(bytes) {
|
|
3
|
+
let s = "";
|
|
4
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
5
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
6
|
+
}
|
|
7
|
+
function randomToken() {
|
|
8
|
+
return b64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/checkout.ts
|
|
12
|
+
var ORIGINS = ["https://chaindaddy.io", "https://staging.chaindaddy.io"];
|
|
13
|
+
var windowName = () => "cd-checkout-" + randomToken();
|
|
14
|
+
var FEATURES = "popup,width=480,height=820";
|
|
15
|
+
var TICK_MS = 300;
|
|
16
|
+
var QUIET_TICKS = 7;
|
|
17
|
+
function parseCheckoutUrl(url) {
|
|
18
|
+
if (typeof url !== "string") return null;
|
|
19
|
+
let u;
|
|
20
|
+
try {
|
|
21
|
+
u = new URL(url);
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const m = /^\/_pay\/([\w-]+)$/.exec(u.pathname);
|
|
26
|
+
return m && ORIGINS.includes(u.origin) && !u.username && !u.password ? { href: u.origin + u.pathname, origin: u.origin, id: m[1] } : null;
|
|
27
|
+
}
|
|
28
|
+
function successTarget(successUrl, id) {
|
|
29
|
+
if (typeof successUrl !== "string") return null;
|
|
30
|
+
let u;
|
|
31
|
+
try {
|
|
32
|
+
u = new URL(successUrl, location.href);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (!/^https?:$/.test(u.protocol) || u.username || u.password) return null;
|
|
37
|
+
const key = (pair) => {
|
|
38
|
+
const raw = pair.split("=")[0];
|
|
39
|
+
try {
|
|
40
|
+
return decodeURIComponent(raw.replace(/\+/g, " "));
|
|
41
|
+
} catch {
|
|
42
|
+
return raw;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const pairs = u.search.slice(1).split("&").filter((pair) => pair && key(pair) !== "session_id");
|
|
46
|
+
pairs.push("session_id=" + encodeURIComponent(id));
|
|
47
|
+
return u.origin + u.pathname + "?" + pairs.join("&") + u.hash;
|
|
48
|
+
}
|
|
49
|
+
var current;
|
|
50
|
+
function wait(popup, target, success) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
let ready = false;
|
|
53
|
+
let quiet = 0;
|
|
54
|
+
let last;
|
|
55
|
+
const done = (result) => {
|
|
56
|
+
clearInterval(timer);
|
|
57
|
+
clearTimeout(last);
|
|
58
|
+
window.removeEventListener("message", onMessage);
|
|
59
|
+
if (success && result.status === "completed") location.assign(success);
|
|
60
|
+
resolve(result);
|
|
61
|
+
};
|
|
62
|
+
const onMessage = (e) => {
|
|
63
|
+
if (e.origin !== target.origin || e.source !== popup) return;
|
|
64
|
+
const data = e.data;
|
|
65
|
+
if (!data || typeof data !== "object") return;
|
|
66
|
+
if (data.type === "cd:checkout.ready") ready = true;
|
|
67
|
+
const s = data.session;
|
|
68
|
+
if (!s || typeof s !== "object" || s.id !== target.id) return;
|
|
69
|
+
if (data.type === "cd:checkout.cancelled") done({ status: "cancelled", session: { id: target.id } });
|
|
70
|
+
else if (data.type === "cd:checkout.completed" && (s.status === "complete" || s.status === "paid")) {
|
|
71
|
+
const session = { id: target.id, status: s.status };
|
|
72
|
+
if (typeof s.orderId === "string" && /^[\w-]{1,128}$/.test(s.orderId)) session.orderId = s.orderId;
|
|
73
|
+
done({ status: "completed", session });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const hello = () => {
|
|
77
|
+
try {
|
|
78
|
+
popup.postMessage({ type: "cd:checkout.hello" }, target.origin);
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const timer = setInterval(() => {
|
|
83
|
+
if (popup.closed) {
|
|
84
|
+
clearInterval(timer);
|
|
85
|
+
last = setTimeout(() => done({ status: "closed" }), TICK_MS);
|
|
86
|
+
} else if (!ready || ++quiet % QUIET_TICKS === 0) hello();
|
|
87
|
+
}, TICK_MS);
|
|
88
|
+
window.addEventListener("message", onMessage);
|
|
89
|
+
hello();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function checkout(o) {
|
|
93
|
+
const refused = () => new Error("checkout: url is not a Chain Daddy checkout session link");
|
|
94
|
+
const track = (popup, result, href) => {
|
|
95
|
+
current = { popup, result, href };
|
|
96
|
+
const over = () => {
|
|
97
|
+
if (current && current.popup === popup) current = void 0;
|
|
98
|
+
};
|
|
99
|
+
result.then(over, over);
|
|
100
|
+
return result;
|
|
101
|
+
};
|
|
102
|
+
const byLink = (target) => {
|
|
103
|
+
location.assign(target.href);
|
|
104
|
+
return { status: "redirected" };
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
const { url, successUrl } = o;
|
|
108
|
+
if (successUrl && !successTarget(successUrl, "")) throw new Error("checkout: successUrl is not a web address");
|
|
109
|
+
const success = (t) => successUrl ? successTarget(successUrl, t.id) : null;
|
|
110
|
+
const target = typeof url === "function" ? null : parseCheckoutUrl(url);
|
|
111
|
+
if (typeof url !== "function" && !target) throw refused();
|
|
112
|
+
if (current && !current.popup.closed && (!target || current.href === target.href)) {
|
|
113
|
+
current.popup.focus();
|
|
114
|
+
return current.result;
|
|
115
|
+
}
|
|
116
|
+
if (target) {
|
|
117
|
+
const popup2 = window.open(target.href, windowName(), FEATURES);
|
|
118
|
+
return popup2 ? track(popup2, wait(popup2, target, success(target)), target.href) : Promise.resolve(byLink(target));
|
|
119
|
+
}
|
|
120
|
+
const popup = window.open("", windowName(), FEATURES);
|
|
121
|
+
const result = new Promise((resolve) => resolve(url())).then(
|
|
122
|
+
(fetched) => {
|
|
123
|
+
const t = parseCheckoutUrl(fetched);
|
|
124
|
+
if (!t) {
|
|
125
|
+
popup?.close();
|
|
126
|
+
throw refused();
|
|
127
|
+
}
|
|
128
|
+
if (!popup) return byLink(t);
|
|
129
|
+
popup.location.href = t.href;
|
|
130
|
+
return wait(popup, t, success(t));
|
|
131
|
+
},
|
|
132
|
+
(e) => {
|
|
133
|
+
popup?.close();
|
|
134
|
+
throw e;
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
return popup ? track(popup, result) : result;
|
|
138
|
+
} catch (e) {
|
|
139
|
+
return Promise.reject(e);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
var bound = false;
|
|
143
|
+
var reporting;
|
|
144
|
+
function bindCheckoutButtons() {
|
|
145
|
+
if (bound || typeof document === "undefined") return;
|
|
146
|
+
bound = true;
|
|
147
|
+
document.addEventListener("click", (e) => {
|
|
148
|
+
const el = e.target instanceof Element ? e.target.closest("[data-cd-checkout]") : null;
|
|
149
|
+
if (!el || e.defaultPrevented || e.button || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
150
|
+
const url = el.getAttribute("data-cd-checkout") || el.href;
|
|
151
|
+
const successUrl = el.getAttribute("data-cd-success");
|
|
152
|
+
if (!parseCheckoutUrl(url) || successUrl && !successTarget(successUrl, "")) return;
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
const visit = checkout({ url, successUrl });
|
|
155
|
+
if (visit === reporting) return;
|
|
156
|
+
reporting = visit;
|
|
157
|
+
visit.then(
|
|
158
|
+
(detail) => void el.dispatchEvent(new CustomEvent("cd:checkout", { detail, bubbles: true })),
|
|
159
|
+
() => {
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
bindCheckoutButtons,
|
|
166
|
+
checkout,
|
|
167
|
+
parseCheckoutUrl
|
|
168
|
+
};
|
package/dist/embed.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The embed origin for a build loaded from scriptSrc: staging's CDN frames staging, anything else prod. */
|
|
2
|
+
export declare function embedOriginFor(scriptSrc?: string | null): string;
|
|
3
|
+
export interface EmbedOptions {
|
|
4
|
+
/** The embed origin. Only the two Chain Daddy embed origins are taken; anything else is prod. */
|
|
5
|
+
origin?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Defines <cd-app>, <cd-coin> and <cd-buy>, each once. cd.js calls it for you. */
|
|
8
|
+
export declare function defineEmbedElements(options?: EmbedOptions): void;
|
package/dist/embed.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// src/embed.ts
|
|
2
|
+
var PROD_ORIGIN = "https://embed.chaindaddy.io";
|
|
3
|
+
var STAGING_ORIGIN = "https://embed-staging.chaindaddy.io";
|
|
4
|
+
var READY_TIMEOUT_MS = 8e3;
|
|
5
|
+
var MAX_HEIGHT = 4e3;
|
|
6
|
+
var THEME_TOKENS = ["--cd-accent", "--cd-surface", "--cd-text", "--cd-text-muted", "--cd-border"];
|
|
7
|
+
function embedOriginFor(scriptSrc) {
|
|
8
|
+
try {
|
|
9
|
+
if (scriptSrc && new URL(scriptSrc).hostname === "js.staging.chaindaddy.io") return STAGING_ORIGIN;
|
|
10
|
+
} catch {
|
|
11
|
+
}
|
|
12
|
+
return PROD_ORIGIN;
|
|
13
|
+
}
|
|
14
|
+
var STYLE = `
|
|
15
|
+
:host { display: block; }
|
|
16
|
+
iframe { display: block; width: 100%; border: 0; height: 1px; opacity: 0; color-scheme: normal; }
|
|
17
|
+
:host([data-cd-ready]) iframe { opacity: 1; }
|
|
18
|
+
:host([data-cd-ready]) slot { display: none; }
|
|
19
|
+
:host([data-cd-unavailable]) iframe { display: none; }
|
|
20
|
+
`;
|
|
21
|
+
var BUY_STYLE = `
|
|
22
|
+
:host { display: inline-block; }
|
|
23
|
+
button { box-sizing: border-box; min-height: 40px; padding: 0 16px; border-radius: 10px; cursor: pointer;
|
|
24
|
+
font: 600 15px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
25
|
+
border: 1px solid var(--cd-border, #d0d5dd); background: var(--cd-accent, #f59e0b); color: var(--cd-text, #101828); }
|
|
26
|
+
button:focus-visible { outline: 2px solid var(--cd-accent, #f59e0b); outline-offset: 2px; }
|
|
27
|
+
`;
|
|
28
|
+
function defineEmbedElements(options = {}) {
|
|
29
|
+
try {
|
|
30
|
+
if (typeof customElements === "undefined" || typeof HTMLElement === "undefined") return;
|
|
31
|
+
const origin = options.origin === STAGING_ORIGIN ? STAGING_ORIGIN : PROD_ORIGIN;
|
|
32
|
+
class CdFrame extends HTMLElement {
|
|
33
|
+
constructor() {
|
|
34
|
+
super(...arguments);
|
|
35
|
+
this.kind = "app";
|
|
36
|
+
this.frame = null;
|
|
37
|
+
this.onMessage = (event) => this.receive(event);
|
|
38
|
+
}
|
|
39
|
+
connectedCallback() {
|
|
40
|
+
try {
|
|
41
|
+
if (!this.shadowRoot) this.attachShadow({ mode: "open" });
|
|
42
|
+
this.mount();
|
|
43
|
+
} catch {
|
|
44
|
+
this.unavailable();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
disconnectedCallback() {
|
|
48
|
+
try {
|
|
49
|
+
window.removeEventListener("message", this.onMessage);
|
|
50
|
+
clearTimeout(this.timer);
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
attributeChangedCallback() {
|
|
55
|
+
try {
|
|
56
|
+
if (this.isConnected && this.shadowRoot) this.mount();
|
|
57
|
+
} catch {
|
|
58
|
+
this.unavailable();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** The frame's address, or null when the element does not name enough. */
|
|
62
|
+
src() {
|
|
63
|
+
const crown = (this.getAttribute("crown") ?? "").trim();
|
|
64
|
+
const chain = (this.getAttribute("chain") ?? "").trim();
|
|
65
|
+
const app = (this.getAttribute("app") ?? "").trim();
|
|
66
|
+
if (!/^[0-9]{1,18}$/.test(crown) || !chain || this.kind === "app" && !app) return null;
|
|
67
|
+
const q = new URLSearchParams({ crown, chain });
|
|
68
|
+
if (this.kind === "app") q.set("app", app);
|
|
69
|
+
else q.set("kind", "coin");
|
|
70
|
+
q.set("mode", this.mode());
|
|
71
|
+
q.set("v", "1");
|
|
72
|
+
return `${origin}/v1/frame?${q.toString()}`;
|
|
73
|
+
}
|
|
74
|
+
mode() {
|
|
75
|
+
const attr = this.getAttribute("mode");
|
|
76
|
+
if (attr === "light" || attr === "dark") return attr;
|
|
77
|
+
try {
|
|
78
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
79
|
+
} catch {
|
|
80
|
+
return "light";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
mount() {
|
|
84
|
+
const root = this.shadowRoot;
|
|
85
|
+
window.removeEventListener("message", this.onMessage);
|
|
86
|
+
clearTimeout(this.timer);
|
|
87
|
+
this.removeAttribute("data-cd-ready");
|
|
88
|
+
this.removeAttribute("data-cd-unavailable");
|
|
89
|
+
const src = this.src();
|
|
90
|
+
if (!src) {
|
|
91
|
+
root.innerHTML = "<slot></slot>";
|
|
92
|
+
this.frame = null;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
root.innerHTML = `<style>${STYLE}</style><slot></slot>`;
|
|
96
|
+
const frame = document.createElement("iframe");
|
|
97
|
+
frame.setAttribute("loading", "lazy");
|
|
98
|
+
frame.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
|
|
99
|
+
frame.setAttribute("allow", "clipboard-write; fullscreen");
|
|
100
|
+
frame.setAttribute("title", this.kind === "coin" ? "Coin on Chain Daddy" : "App on Chain Daddy");
|
|
101
|
+
frame.src = src;
|
|
102
|
+
root.appendChild(frame);
|
|
103
|
+
this.frame = frame;
|
|
104
|
+
window.addEventListener("message", this.onMessage);
|
|
105
|
+
this.timer = setTimeout(() => {
|
|
106
|
+
if (!this.hasAttribute("data-cd-ready")) this.unavailable();
|
|
107
|
+
}, READY_TIMEOUT_MS);
|
|
108
|
+
}
|
|
109
|
+
receive(event) {
|
|
110
|
+
try {
|
|
111
|
+
if (event.origin !== origin || !this.frame || !event.source || event.source !== this.frame.contentWindow) return;
|
|
112
|
+
const data = event.data;
|
|
113
|
+
if (!data || typeof data !== "object") return;
|
|
114
|
+
if (data.type === "cd:ready") {
|
|
115
|
+
clearTimeout(this.timer);
|
|
116
|
+
this.removeAttribute("data-cd-unavailable");
|
|
117
|
+
this.setAttribute("data-cd-ready", "");
|
|
118
|
+
this.sendTheme();
|
|
119
|
+
this.dispatchEvent(new CustomEvent("cd:ready"));
|
|
120
|
+
} else if (data.type === "cd:resize") {
|
|
121
|
+
const h = Number(data.height);
|
|
122
|
+
if (Number.isFinite(h)) this.frame.style.height = `${Math.max(1, Math.min(MAX_HEIGHT, Math.ceil(h)))}px`;
|
|
123
|
+
} else if (data.type === "cd:unavailable") {
|
|
124
|
+
this.unavailable();
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** The page's --cd-* colours, for an app that takes them. */
|
|
130
|
+
sendTheme() {
|
|
131
|
+
const tokens = {};
|
|
132
|
+
const style = getComputedStyle(this);
|
|
133
|
+
for (const token of THEME_TOKENS) {
|
|
134
|
+
const value = style.getPropertyValue(token).trim();
|
|
135
|
+
if (value) tokens[token] = value;
|
|
136
|
+
}
|
|
137
|
+
this.frame?.contentWindow?.postMessage({ type: "cd:theme", tokens }, origin);
|
|
138
|
+
}
|
|
139
|
+
unavailable() {
|
|
140
|
+
try {
|
|
141
|
+
clearTimeout(this.timer);
|
|
142
|
+
this.removeAttribute("data-cd-ready");
|
|
143
|
+
if (this.hasAttribute("data-cd-unavailable")) return;
|
|
144
|
+
this.setAttribute("data-cd-unavailable", "");
|
|
145
|
+
this.dispatchEvent(new CustomEvent("cd:unavailable"));
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
CdFrame.observedAttributes = ["crown", "chain", "app", "mode"];
|
|
151
|
+
class CdApp extends CdFrame {
|
|
152
|
+
}
|
|
153
|
+
class CdCoin extends CdFrame {
|
|
154
|
+
constructor() {
|
|
155
|
+
super(...arguments);
|
|
156
|
+
this.kind = "coin";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
class CdBuy extends HTMLElement {
|
|
160
|
+
connectedCallback() {
|
|
161
|
+
try {
|
|
162
|
+
const link = this.querySelector("a[href]");
|
|
163
|
+
const href = link ? new URL(link.getAttribute("href") ?? "", document.baseURI) : null;
|
|
164
|
+
if (!href || href.protocol !== "https:") return;
|
|
165
|
+
const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
|
|
166
|
+
root.innerHTML = `<style>${BUY_STYLE}</style><button type="button" part="button"></button>`;
|
|
167
|
+
const button = root.querySelector("button");
|
|
168
|
+
button.textContent = (link.textContent ?? "").trim() || "Buy on Chain Daddy";
|
|
169
|
+
button.addEventListener("click", () => {
|
|
170
|
+
try {
|
|
171
|
+
if (!window.open(href.toString(), "cd-buy", "popup,width=460,height=760")) window.location.assign(href.toString());
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const [name, ctor] of [["cd-app", CdApp], ["cd-coin", CdCoin], ["cd-buy", CdBuy]]) {
|
|
180
|
+
if (!customElements.get(name)) customElements.define(name, ctor);
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
defineEmbedElements,
|
|
187
|
+
embedOriginFor
|
|
188
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -2,3 +2,7 @@ export { VERSION } from './core';
|
|
|
2
2
|
export { DEFAULT_ISSUER, challengeFor, exchangeCode, handleCallback, signIn } from './signin';
|
|
3
3
|
export type { SignInOptions, SignInResult } from './signin';
|
|
4
4
|
export { defineSignInButton } from './button';
|
|
5
|
+
export { defineEmbedElements, embedOriginFor } from './embed';
|
|
6
|
+
export type { EmbedOptions } from './embed';
|
|
7
|
+
export { bindCheckoutButtons, checkout, parseCheckoutUrl } from './checkout';
|
|
8
|
+
export type { CheckoutOptions, CheckoutResult, CheckoutTarget } from './checkout';
|