@chaindaddy/js 0.2.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 CHANGED
@@ -864,6 +864,72 @@ export interface paths {
864
864
  patch?: never;
865
865
  trace?: never;
866
866
  };
867
+ "/api/v2/iap/checkout/sessions": {
868
+ parameters: {
869
+ query?: never;
870
+ header?: never;
871
+ path?: never;
872
+ cookie?: never;
873
+ };
874
+ get?: never;
875
+ put?: never;
876
+ /**
877
+ * Open a checkout session
878
+ * @description Take a payment for your coin's store on your own website. Your server calls this with a store key
879
+ * (scope `orders:write`) and gets back `url`: send the buyer there with a plain link or a redirect. The
880
+ * buyer pays on chaindaddy.io and returns to `successUrl`, or to `cancelUrl` if they leave.
881
+ *
882
+ * A session sells one item (`lineItems` takes exactly one entry). The price, the fee and where the money
883
+ * goes are the item's own, exactly as when it is bought on chaindaddy.io: a session changes none of them.
884
+ *
885
+ * `successUrl` and `cancelUrl` must be `https` pages on one of the coin's verified sites, or a subdomain of
886
+ * one (400 `RETURN_URL_NOT_VERIFIED`, which lists the sites that pass). The wallet that opened the store
887
+ * must have signed the Seller Terms (403 `SELLER_TERMS_REQUIRED`). An item that belongs to an installed app
888
+ * cannot be sold through a session (409 `SKU_NOT_FOR_SALE`).
889
+ *
890
+ * Send an `Idempotency-Key`. A retry of the same request returns the session it opened, with status 200
891
+ * and the header `Idempotent-Replayed: true`.
892
+ *
893
+ * Learn that the session was paid from the `iap.order.confirmed` webhook (its payload carries
894
+ * `checkoutSessionId`), or by reading the session. Never from the buyer arriving on `successUrl`.
895
+ *
896
+ * Answers 404 `CHECKOUT_SESSIONS_DISABLED` where checkout sessions are not switched on, or are open to
897
+ * test stores only and the key is a live store's. `GET /api/v2/iap/config` says which (`checkoutSessions`).
898
+ * Guide: https://docs.chaindaddy.io/developer/take-payments-on-your-site
899
+ */
900
+ post: operations["createIapCheckoutSession"];
901
+ delete?: never;
902
+ options?: never;
903
+ head?: never;
904
+ patch?: never;
905
+ trace?: never;
906
+ };
907
+ "/api/v2/iap/checkout/sessions/{sessionId}": {
908
+ parameters: {
909
+ query?: never;
910
+ header?: never;
911
+ path: {
912
+ /** @description The checkout session's id, `cs_` followed by 48 hex characters */
913
+ sessionId: components["parameters"]["IapCheckoutSessionId"];
914
+ };
915
+ cookie?: never;
916
+ };
917
+ /**
918
+ * Get a checkout session
919
+ * @description Your server reads a session it opened, with a store key of the same store (scope `orders:read`).
920
+ * `status` is `open`, `complete` once the buyer's payment is confirmed, or `expired`. `order` is the order
921
+ * that paid it (or the buyer's latest attempt while it is open), and `receipt` is that order's signed
922
+ * receipt once the session is complete. A key of any other store gets 404.
923
+ */
924
+ get: operations["getIapCheckoutSession"];
925
+ put?: never;
926
+ post?: never;
927
+ delete?: never;
928
+ options?: never;
929
+ head?: never;
930
+ patch?: never;
931
+ trace?: never;
932
+ };
867
933
  "/api/v2/iap/checkout/sessions/{sessionId}/public": {
868
934
  parameters: {
869
935
  query?: never;
@@ -882,6 +948,10 @@ export interface paths {
882
948
  *
883
949
  * Pass `origin` to learn whether a page on that origin may open this checkout in a popup
884
950
  * (`openerAllowed`): it must be `https` on one of the coin's verified sites.
951
+ *
952
+ * `returnAllowed` says whether the buyer may still be sent to `successUrl` and `cancelUrl`. It is checked
953
+ * again on every read, so a coin whose site stopped being verified after the session was opened no longer
954
+ * gets its buyers sent there; the pay page then shows the result itself.
885
955
  */
886
956
  get: operations["getIapPublicCheckoutSession"];
887
957
  put?: never;
@@ -1852,6 +1922,46 @@ export interface components {
1852
1922
  wallet?: string;
1853
1923
  entitlements?: components["schemas"]["IapEntitlement"][];
1854
1924
  };
1925
+ /** @description A checkout session, as the store's own server sees it */
1926
+ IapCheckoutSession: {
1927
+ /** @description `cs_` followed by 48 hex characters */
1928
+ id?: string;
1929
+ /**
1930
+ * @description The page the buyer pays on. Link or redirect the buyer to it
1931
+ * @example https://chaindaddy.io/_pay/cs_0f3a9c1e5b7d2a4c6e8f0b1d3a5c7e9f1b3d5a7c9e0f2a4c
1932
+ */
1933
+ url?: string;
1934
+ /**
1935
+ * @description `complete` once the payment is confirmed; `expired` when it was not paid by `expiresAt`
1936
+ * @enum {string}
1937
+ */
1938
+ status?: "open" | "complete" | "expired";
1939
+ /** @description False for a test store's session */
1940
+ livemode?: boolean;
1941
+ /** Format: date-time */
1942
+ expiresAt?: string;
1943
+ /** Format: date-time */
1944
+ completedAt?: string | null;
1945
+ /** Format: date-time */
1946
+ createdAt?: string;
1947
+ storeId?: string;
1948
+ lineItems?: {
1949
+ sku?: string;
1950
+ quantity?: number;
1951
+ }[];
1952
+ successUrl?: string;
1953
+ cancelUrl?: string;
1954
+ clientReferenceId?: string | null;
1955
+ metadata?: {
1956
+ [key: string]: unknown;
1957
+ };
1958
+ /** @description The only wallet that can pay, when the session names one */
1959
+ wallet?: string | null;
1960
+ /** @description The order that paid the session, or the buyer's latest attempt while it is open; null until the buyer starts paying */
1961
+ order?: components["schemas"]["IapOrder"] | null;
1962
+ /** @description The paying order's signed receipt (a JWS), once the session is complete */
1963
+ receipt?: string | null;
1964
+ };
1855
1965
  /** @description A checkout session, as the buyer's pay page sees it. Never carries `metadata` or `clientReferenceId` */
1856
1966
  IapPublicCheckoutSession: {
1857
1967
  id?: string;
@@ -1869,6 +1979,11 @@ export interface components {
1869
1979
  wallet?: string | null;
1870
1980
  /** @description Whether a page on the `origin` passed may open this checkout in a popup. False when no `origin` is passed */
1871
1981
  openerAllowed?: boolean;
1982
+ /**
1983
+ * @description Whether `successUrl` and `cancelUrl` are both still on the coin's verified sites, as of this read.
1984
+ * When false the buyer is not sent to either
1985
+ */
1986
+ returnAllowed?: boolean;
1872
1987
  };
1873
1988
  IapSkuOwners: {
1874
1989
  entries?: {
@@ -4256,7 +4371,9 @@ export interface operations {
4256
4371
  /**
4257
4372
  * @description The checkout session the buyer is paying. A buyer's session only: a store key gets 403
4258
4373
  * `SESSION_REQUIRED`. 409 `CHECKOUT_SESSION_EXPIRED` or `CHECKOUT_SESSION_COMPLETE` when the
4259
- * session can no longer be paid; 403 `CHECKOUT_SESSION_WALLET` when it names another wallet
4374
+ * session can no longer be paid; 403 `CHECKOUT_SESSION_WALLET` when it names another wallet.
4375
+ * A session takes one buyer at a time: 409 `CHECKOUT_SESSION_IN_USE` while another wallet has
4376
+ * an order for it that can still be paid, or is paid and being confirmed
4260
4377
  */
4261
4378
  checkoutSessionId?: string;
4262
4379
  /** @description Required with a store key; the wallet that receives the items */
@@ -4367,6 +4484,186 @@ export interface operations {
4367
4484
  404: components["responses"]["IapNotFound"];
4368
4485
  };
4369
4486
  };
4487
+ createIapCheckoutSession: {
4488
+ parameters: {
4489
+ query?: never;
4490
+ header?: {
4491
+ /**
4492
+ * @description Send a unique value (a UUID) with every POST. Retrying with the same key returns the first result
4493
+ * and never sells, grants or consumes twice.
4494
+ */
4495
+ "Idempotency-Key"?: components["parameters"]["IdempotencyKey"];
4496
+ };
4497
+ path?: never;
4498
+ cookie?: never;
4499
+ };
4500
+ requestBody: {
4501
+ content: {
4502
+ "application/json": {
4503
+ /** @description What is sold. Exactly one item */
4504
+ lineItems: {
4505
+ /** @description The item's SKU key in your store */
4506
+ sku: string;
4507
+ /** @default 1 */
4508
+ quantity?: number;
4509
+ }[];
4510
+ /**
4511
+ * Format: uri
4512
+ * @description Where the buyer returns after paying. `https`, on one of the coin's verified sites
4513
+ */
4514
+ successUrl: string;
4515
+ /**
4516
+ * Format: uri
4517
+ * @description Where the buyer returns if they leave without paying. Same rule as `successUrl`
4518
+ */
4519
+ cancelUrl: string;
4520
+ /**
4521
+ * @description Your own id for this checkout (a cart or user id). Echoed as `appAccountToken` on the order,
4522
+ * its events and its receipt, which the buyer can read: a reference, not a secret
4523
+ */
4524
+ clientReferenceId?: string;
4525
+ /** @description Your own data, returned when you read the session. At most 20 keys and 4 KB. Never shown to the buyer */
4526
+ metadata?: {
4527
+ [key: string]: unknown;
4528
+ };
4529
+ /** @description When set, only this wallet can pay the session. An address on the store's chain */
4530
+ wallet?: string;
4531
+ /**
4532
+ * @description How long the session can be paid
4533
+ * @default 3600
4534
+ */
4535
+ expiresInSeconds?: number;
4536
+ };
4537
+ };
4538
+ };
4539
+ responses: {
4540
+ /** @description A retry of a request already made under this `Idempotency-Key`; the session it opened */
4541
+ 200: {
4542
+ headers: {
4543
+ "Idempotent-Replayed"?: "true";
4544
+ [name: string]: unknown;
4545
+ };
4546
+ content: {
4547
+ "application/json": components["schemas"]["IapCheckoutSession"];
4548
+ };
4549
+ };
4550
+ /** @description The session. Send the buyer to `url` */
4551
+ 201: {
4552
+ headers: {
4553
+ [name: string]: unknown;
4554
+ };
4555
+ content: {
4556
+ "application/json": components["schemas"]["IapCheckoutSession"];
4557
+ };
4558
+ };
4559
+ /**
4560
+ * @description `INVALID_REQUEST`, `IDEMPOTENCY_KEY_REQUIRED`, or `RETURN_URL_NOT_VERIFIED`: a return URL is not an
4561
+ * `https` page on one of the coin's verified sites
4562
+ */
4563
+ 400: {
4564
+ headers: {
4565
+ [name: string]: unknown;
4566
+ };
4567
+ content: {
4568
+ "application/json": components["schemas"]["IapError"];
4569
+ };
4570
+ };
4571
+ /** @description No store key, or one that is unknown or revoked (`INVALID_STORE_KEY`) */
4572
+ 401: {
4573
+ headers: {
4574
+ [name: string]: unknown;
4575
+ };
4576
+ content: {
4577
+ "application/json": components["schemas"]["IapError"];
4578
+ };
4579
+ };
4580
+ /**
4581
+ * @description `STORE_KEY_REQUIRED`: called with a signed-in session instead of a store key. `SCOPE_REQUIRED`: the key
4582
+ * lacks `orders:write`. `STORE_NOT_LIVE`: the store is paused or its plan lapsed. `SELLER_TERMS_REQUIRED`:
4583
+ * the wallet that opened the store has not signed the Seller Terms
4584
+ */
4585
+ 403: {
4586
+ headers: {
4587
+ [name: string]: unknown;
4588
+ };
4589
+ content: {
4590
+ "application/json": components["schemas"]["IapError"];
4591
+ };
4592
+ };
4593
+ /** @description `SKU_NOT_FOR_SALE`: no such item in the store. `CHECKOUT_SESSIONS_DISABLED`: not switched on here */
4594
+ 404: {
4595
+ headers: {
4596
+ [name: string]: unknown;
4597
+ };
4598
+ content: {
4599
+ "application/json": components["schemas"]["IapError"];
4600
+ };
4601
+ };
4602
+ /** @description `SKU_NOT_FOR_SALE`: the item is archived, has no price, is an unlock, or belongs to an installed app */
4603
+ 409: {
4604
+ headers: {
4605
+ [name: string]: unknown;
4606
+ };
4607
+ content: {
4608
+ "application/json": components["schemas"]["IapError"];
4609
+ };
4610
+ };
4611
+ /**
4612
+ * @description `IDEMPOTENCY_KEY_REUSED`: this `Idempotency-Key` already opened a session for a different request.
4613
+ * Nothing is returned or created; use a new key for a new session
4614
+ */
4615
+ 422: {
4616
+ headers: {
4617
+ [name: string]: unknown;
4618
+ };
4619
+ content: {
4620
+ "application/json": components["schemas"]["IapError"];
4621
+ };
4622
+ };
4623
+ };
4624
+ };
4625
+ getIapCheckoutSession: {
4626
+ parameters: {
4627
+ query?: never;
4628
+ header?: never;
4629
+ path: {
4630
+ /** @description The checkout session's id, `cs_` followed by 48 hex characters */
4631
+ sessionId: components["parameters"]["IapCheckoutSessionId"];
4632
+ };
4633
+ cookie?: never;
4634
+ };
4635
+ requestBody?: never;
4636
+ responses: {
4637
+ /** @description The session */
4638
+ 200: {
4639
+ headers: {
4640
+ [name: string]: unknown;
4641
+ };
4642
+ content: {
4643
+ "application/json": components["schemas"]["IapCheckoutSession"];
4644
+ };
4645
+ };
4646
+ /** @description No store key, or one that is unknown or revoked */
4647
+ 401: {
4648
+ headers: {
4649
+ [name: string]: unknown;
4650
+ };
4651
+ content: {
4652
+ "application/json": components["schemas"]["IapError"];
4653
+ };
4654
+ };
4655
+ /** @description `STORE_KEY_REQUIRED` (a signed-in session instead of a store key) or `SCOPE_REQUIRED` (the key lacks `orders:read`) */
4656
+ 403: {
4657
+ headers: {
4658
+ [name: string]: unknown;
4659
+ };
4660
+ content: {
4661
+ "application/json": components["schemas"]["IapError"];
4662
+ };
4663
+ };
4664
+ 404: components["responses"]["IapNotFound"];
4665
+ };
4666
+ };
4370
4667
  getIapPublicCheckoutSession: {
4371
4668
  parameters: {
4372
4669
  query?: {
package/dist/cd.js CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";(()=>{var w="0.2.0";function b(t){let e="";for(let i of t)e+=String.fromCharCode(i);return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function g(){return b(crypto.getRandomValues(new Uint8Array(32)))}function v(t,e,i,s){let c=a=>{if(a&&typeof a=="object"&&a.type===t)try{i(a)}catch{}},u=a=>{a.origin===e&&c(a.data)};window.addEventListener("message",u);let d=null;if(s&&typeof BroadcastChannel=="function"&&e===location.origin)try{d=new BroadcastChannel(s),d.onmessage=a=>c(a.data)}catch{d=null}return()=>{window.removeEventListener("message",u),d?.close()}}var h={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 A="https://api.chaindaddy.io",T={"https://api.chaindaddy.io":"https://chaindaddy.io/_oauth/authorize","https://staging-api.chaindaddy.io":"https://staging.chaindaddy.io/_oauth/authorize"},p="cd-signin",E="cd:signin",m="cd.signin";async function U(t){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t));return b(new Uint8Array(e))}function M(t="openid"){let e=t.split(/\s+/).filter(Boolean);return(e.includes("openid")?e:["openid",...e]).join(" ")}async function L(t,e,i){let s=new URLSearchParams({response_type:"code",client_id:e.clientId,redirect_uri:e.redirectUri,scope:M(t.scope),state:e.state,nonce:e.nonce,code_challenge:await U(e.verifier),code_challenge_method:"S256"});return t.claims&&s.set("claims",typeof t.claims=="string"?t.claims:JSON.stringify(t.claims)),`${i}?${s}`}function x(t,e){return{code:e,codeVerifier:t.verifier,state:t.state,nonce:t.nonce,issuer:t.issuer,clientId:t.clientId,redirectUri:t.redirectUri}}function f(t){let e=(t.issuer??A).replace(/\/$/,""),i=t.authorizeUrl??T[e];if(!i)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:g(),nonce:g(),verifier:g(),issuer:e,clientId:t.clientId,redirectUri:t.redirectUri};h.set(m,JSON.stringify(s));let c=window.open("",p,"popup,width=480,height=720");return(async()=>{let u=await L(t,s,i);if(!c)return location.assign(u),null;c.location.href=u;let d=new URL(t.redirectUri,location.href).origin;return new Promise((a,n)=>{let o=v(E,d,r=>{r.state===s.state&&(o(),h.remove(m),typeof r.error=="string"&&r.error?n(new Error(r.error)):r.iss!==e?n(new Error("the answer came from another issuer")):typeof r.code!="string"||!r.code?n(new Error("the answer carried no code")):a(x(s,r.code)))},p);t.signal?.addEventListener("abort",()=>{o(),n(new Error("aborted"))})})})()}async function S(){let t=new URLSearchParams(location.search),e={type:E,code:t.get("code"),state:t.get("state"),iss:t.get("iss"),error:t.get("error")};if(window.name===p){try{window.opener?.postMessage(e,location.origin)}catch{}try{let c=new BroadcastChannel(p);c.postMessage(e),c.close()}catch{}return window.close(),null}let i=h.get(m);h.remove(m);let s=i?JSON.parse(i):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 x(s,e.code)}async function k(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})}),i=await e.json();if(!e.ok)throw new Error(`${i.error??e.status}: ${i.error_description??""}`.trim());return i}var O='<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>',P=`
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,16 +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 I(){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 i=this.getAttribute("text")==="continue"?"Continue with Chain Daddy":"Sign in with Chain Daddy",s=this.getAttribute("theme")==="dark"?"dark":"light";this.root.innerHTML=`<style>${P}</style><button type="button" part="button" data-theme="${s}">${O}<span></span></button>`;let c=this.root.querySelector("button");c.querySelector("span").textContent=i,c.addEventListener("click",()=>this.start())}start(){let i=s=>this.getAttribute(s)||void 0;f({clientId:i("client-id")??"",redirectUri:i("redirect-uri")??"",scope:i("scope"),claims:i("claims"),issuer:i("issuer"),authorizeUrl:i("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 C="https://embed.chaindaddy.io",y="https://embed-staging.chaindaddy.io";var H=["--cd-accent","--cd-surface","--cd-text","--cd-text-muted","--cd-border"];function _(t){try{if(t&&new URL(t).hostname==="js.staging.chaindaddy.io")return y}catch{}return C}var D=`
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
11
  :host { display: block; }
12
12
  iframe { display: block; width: 100%; border: 0; height: 1px; opacity: 0; color-scheme: normal; }
13
13
  :host([data-cd-ready]) iframe { opacity: 1; }
14
14
  :host([data-cd-ready]) slot { display: none; }
15
15
  :host([data-cd-unavailable]) iframe { display: none; }
16
- `,z=`
16
+ `,Q=`
17
17
  :host { display: inline-block; }
18
18
  button { box-sizing: border-box; min-height: 40px; padding: 0 16px; border-radius: 10px; cursor: pointer;
19
19
  font: 600 15px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
20
20
  border: 1px solid var(--cd-border, #d0d5dd); background: var(--cd-accent, #f59e0b); color: var(--cd-text, #101828); }
21
21
  button:focus-visible { outline: 2px solid var(--cd-accent, #f59e0b); outline-offset: 2px; }
22
- `;function R(t={}){try{if(typeof customElements>"u"||typeof HTMLElement>"u")return;let e=t.origin===y?y:C;class i extends HTMLElement{constructor(){super(...arguments);this.kind="app";this.frame=null;this.onMessage=n=>this.receive(n)}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 n=(this.getAttribute("crown")??"").trim(),o=(this.getAttribute("chain")??"").trim(),r=(this.getAttribute("app")??"").trim();if(!/^[0-9]{1,18}$/.test(n)||!o||this.kind==="app"&&!r)return null;let l=new URLSearchParams({crown:n,chain:o});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 n=this.getAttribute("mode");if(n==="light"||n==="dark")return n;try{return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}catch{return"light"}}mount(){let n=this.shadowRoot;window.removeEventListener("message",this.onMessage),clearTimeout(this.timer),this.removeAttribute("data-cd-ready"),this.removeAttribute("data-cd-unavailable");let o=this.src();if(!o){n.innerHTML="<slot></slot>",this.frame=null;return}n.innerHTML=`<style>${D}</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=o,n.appendChild(r),this.frame=r,window.addEventListener("message",this.onMessage),this.timer=setTimeout(()=>{this.hasAttribute("data-cd-ready")||this.unavailable()},8e3)}receive(n){try{if(n.origin!==e||!this.frame||!n.source||n.source!==this.frame.contentWindow)return;let o=n.data;if(!o||typeof o!="object")return;if(o.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(o.type==="cd:resize"){let r=Number(o.height);Number.isFinite(r)&&(this.frame.style.height=`${Math.max(1,Math.min(4e3,Math.ceil(r)))}px`)}else o.type==="cd:unavailable"&&this.unavailable()}catch{}}sendTheme(){let n={},o=getComputedStyle(this);for(let r of H){let l=o.getPropertyValue(r).trim();l&&(n[r]=l)}this.frame?.contentWindow?.postMessage({type:"cd:theme",tokens:n},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{}}}i.observedAttributes=["crown","chain","app","mode"];class s extends i{}class c extends i{constructor(){super(...arguments);this.kind="coin"}}class u extends HTMLElement{connectedCallback(){try{let a=this.querySelector("a[href]"),n=a?new URL(a.getAttribute("href")??"",document.baseURI):null;if(!n||n.protocol!=="https:")return;let o=this.shadowRoot??this.attachShadow({mode:"open"});o.innerHTML=`<style>${z}</style><button type="button" part="button"></button>`;let r=o.querySelector("button");r.textContent=(a.textContent??"").trim()||"Buy on Chain Daddy",r.addEventListener("click",()=>{try{window.open(n.toString(),"cd-buy","popup,width=460,height=760")||window.location.assign(n.toString())}catch{}})}catch{}}}for(let[d,a]of[["cd-app",s],["cd-coin",c],["cd-buy",u]])customElements.get(d)||customElements.define(d,a)}catch{}}var N={version:w,signIn:f,handleCallback:S,exchangeCode:k};window.ChainDaddy=Object.assign(window.ChainDaddy??{},N);I();R({origin:_(document.currentScript?.src)});})();
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-KKpjQXvfR3Rs+FsdOW+hfEJcZpfuYVXR+TcyRX0nbup+s/xRhLx102TTXL29eFE1
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;
@@ -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/index.d.ts CHANGED
@@ -4,3 +4,5 @@ export type { SignInOptions, SignInResult } from './signin';
4
4
  export { defineSignInButton } from './button';
5
5
  export { defineEmbedElements, embedOriginFor } from './embed';
6
6
  export type { EmbedOptions } from './embed';
7
+ export { bindCheckoutButtons, checkout, parseCheckoutUrl } from './checkout';
8
+ export type { CheckoutOptions, CheckoutResult, CheckoutTarget } from './checkout';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/core.ts
2
- var VERSION = true ? "0.2.0" : "0.0.0";
2
+ var VERSION = true ? "0.3.0" : "0.0.0";
3
3
  function b64url(bytes) {
4
4
  let s = "";
5
5
  for (const b of bytes) s += String.fromCharCode(b);
@@ -397,14 +397,171 @@ function defineEmbedElements(options = {}) {
397
397
  } catch {
398
398
  }
399
399
  }
400
+
401
+ // src/checkout.ts
402
+ var ORIGINS = ["https://chaindaddy.io", "https://staging.chaindaddy.io"];
403
+ var windowName = () => "cd-checkout-" + randomToken();
404
+ var FEATURES = "popup,width=480,height=820";
405
+ var TICK_MS = 300;
406
+ var QUIET_TICKS = 7;
407
+ function parseCheckoutUrl(url) {
408
+ if (typeof url !== "string") return null;
409
+ let u;
410
+ try {
411
+ u = new URL(url);
412
+ } catch {
413
+ return null;
414
+ }
415
+ const m = /^\/_pay\/([\w-]+)$/.exec(u.pathname);
416
+ return m && ORIGINS.includes(u.origin) && !u.username && !u.password ? { href: u.origin + u.pathname, origin: u.origin, id: m[1] } : null;
417
+ }
418
+ function successTarget(successUrl, id) {
419
+ if (typeof successUrl !== "string") return null;
420
+ let u;
421
+ try {
422
+ u = new URL(successUrl, location.href);
423
+ } catch {
424
+ return null;
425
+ }
426
+ if (!/^https?:$/.test(u.protocol) || u.username || u.password) return null;
427
+ const key = (pair) => {
428
+ const raw = pair.split("=")[0];
429
+ try {
430
+ return decodeURIComponent(raw.replace(/\+/g, " "));
431
+ } catch {
432
+ return raw;
433
+ }
434
+ };
435
+ const pairs = u.search.slice(1).split("&").filter((pair) => pair && key(pair) !== "session_id");
436
+ pairs.push("session_id=" + encodeURIComponent(id));
437
+ return u.origin + u.pathname + "?" + pairs.join("&") + u.hash;
438
+ }
439
+ var current;
440
+ function wait(popup, target, success) {
441
+ return new Promise((resolve) => {
442
+ let ready = false;
443
+ let quiet = 0;
444
+ let last;
445
+ const done = (result2) => {
446
+ clearInterval(timer);
447
+ clearTimeout(last);
448
+ window.removeEventListener("message", onMessage);
449
+ if (success && result2.status === "completed") location.assign(success);
450
+ resolve(result2);
451
+ };
452
+ const onMessage = (e) => {
453
+ if (e.origin !== target.origin || e.source !== popup) return;
454
+ const data = e.data;
455
+ if (!data || typeof data !== "object") return;
456
+ if (data.type === "cd:checkout.ready") ready = true;
457
+ const s = data.session;
458
+ if (!s || typeof s !== "object" || s.id !== target.id) return;
459
+ if (data.type === "cd:checkout.cancelled") done({ status: "cancelled", session: { id: target.id } });
460
+ else if (data.type === "cd:checkout.completed" && (s.status === "complete" || s.status === "paid")) {
461
+ const session = { id: target.id, status: s.status };
462
+ if (typeof s.orderId === "string" && /^[\w-]{1,128}$/.test(s.orderId)) session.orderId = s.orderId;
463
+ done({ status: "completed", session });
464
+ }
465
+ };
466
+ const hello = () => {
467
+ try {
468
+ popup.postMessage({ type: "cd:checkout.hello" }, target.origin);
469
+ } catch {
470
+ }
471
+ };
472
+ const timer = setInterval(() => {
473
+ if (popup.closed) {
474
+ clearInterval(timer);
475
+ last = setTimeout(() => done({ status: "closed" }), TICK_MS);
476
+ } else if (!ready || ++quiet % QUIET_TICKS === 0) hello();
477
+ }, TICK_MS);
478
+ window.addEventListener("message", onMessage);
479
+ hello();
480
+ });
481
+ }
482
+ function checkout(o) {
483
+ const refused = () => new Error("checkout: url is not a Chain Daddy checkout session link");
484
+ const track = (popup, result2, href) => {
485
+ current = { popup, result: result2, href };
486
+ const over = () => {
487
+ if (current && current.popup === popup) current = void 0;
488
+ };
489
+ result2.then(over, over);
490
+ return result2;
491
+ };
492
+ const byLink = (target) => {
493
+ location.assign(target.href);
494
+ return { status: "redirected" };
495
+ };
496
+ try {
497
+ const { url, successUrl } = o;
498
+ if (successUrl && !successTarget(successUrl, "")) throw new Error("checkout: successUrl is not a web address");
499
+ const success = (t) => successUrl ? successTarget(successUrl, t.id) : null;
500
+ const target = typeof url === "function" ? null : parseCheckoutUrl(url);
501
+ if (typeof url !== "function" && !target) throw refused();
502
+ if (current && !current.popup.closed && (!target || current.href === target.href)) {
503
+ current.popup.focus();
504
+ return current.result;
505
+ }
506
+ if (target) {
507
+ const popup2 = window.open(target.href, windowName(), FEATURES);
508
+ return popup2 ? track(popup2, wait(popup2, target, success(target)), target.href) : Promise.resolve(byLink(target));
509
+ }
510
+ const popup = window.open("", windowName(), FEATURES);
511
+ const result2 = new Promise((resolve) => resolve(url())).then(
512
+ (fetched) => {
513
+ const t = parseCheckoutUrl(fetched);
514
+ if (!t) {
515
+ popup?.close();
516
+ throw refused();
517
+ }
518
+ if (!popup) return byLink(t);
519
+ popup.location.href = t.href;
520
+ return wait(popup, t, success(t));
521
+ },
522
+ (e) => {
523
+ popup?.close();
524
+ throw e;
525
+ }
526
+ );
527
+ return popup ? track(popup, result2) : result2;
528
+ } catch (e) {
529
+ return Promise.reject(e);
530
+ }
531
+ }
532
+ var bound = false;
533
+ var reporting;
534
+ function bindCheckoutButtons() {
535
+ if (bound || typeof document === "undefined") return;
536
+ bound = true;
537
+ document.addEventListener("click", (e) => {
538
+ const el = e.target instanceof Element ? e.target.closest("[data-cd-checkout]") : null;
539
+ if (!el || e.defaultPrevented || e.button || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
540
+ const url = el.getAttribute("data-cd-checkout") || el.href;
541
+ const successUrl = el.getAttribute("data-cd-success");
542
+ if (!parseCheckoutUrl(url) || successUrl && !successTarget(successUrl, "")) return;
543
+ e.preventDefault();
544
+ const visit = checkout({ url, successUrl });
545
+ if (visit === reporting) return;
546
+ reporting = visit;
547
+ visit.then(
548
+ (detail) => void el.dispatchEvent(new CustomEvent("cd:checkout", { detail, bubbles: true })),
549
+ () => {
550
+ }
551
+ );
552
+ });
553
+ }
400
554
  export {
401
555
  DEFAULT_ISSUER,
402
556
  VERSION,
557
+ bindCheckoutButtons,
403
558
  challengeFor,
559
+ checkout,
404
560
  defineEmbedElements,
405
561
  defineSignInButton,
406
562
  embedOriginFor,
407
563
  exchangeCode,
408
564
  handleCallback,
565
+ parseCheckoutUrl,
409
566
  signIn
410
567
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chaindaddy/js",
3
- "version": "0.2.0",
4
- "description": "Chain Daddy for any website: Sign in with Chain Daddy (a popup and a button), a coin's apps on its own site (<cd-app>), and a server-side check that a store order was paid on chain, with checkout to follow",
3
+ "version": "0.3.0",
4
+ "description": "Chain Daddy for any website: Sign in with Chain Daddy (a popup and a button), a coin's apps on its own site (<cd-app>), checkout for a coin's store in a popup, and a server-side check that a store order was paid on chain",
5
5
  "homepage": "https://docs.chaindaddy.io/developer/javascript-sdk",
6
6
  "repository": {
7
7
  "type": "git",
@@ -30,6 +30,10 @@
30
30
  "types": "./dist/embed.d.ts",
31
31
  "default": "./dist/embed.js"
32
32
  },
33
+ "./checkout": {
34
+ "types": "./dist/checkout.d.ts",
35
+ "default": "./dist/checkout.js"
36
+ },
33
37
  "./verify": {
34
38
  "types": "./dist/verify.d.ts",
35
39
  "default": "./dist/verify.js"