@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 +298 -1
- package/dist/cd.js +4 -4
- package/dist/cd.js.sri +1 -1
- package/dist/checkout.d.ts +73 -0
- package/dist/checkout.js +168 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +158 -1
- package/package.json +6 -2
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
|
|
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
|
|
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
|
-
`,
|
|
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
|
|
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/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
|
+
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.
|
|
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
|
|
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"
|