@klappay/one 1.0.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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.global.js +39 -0
- package/dist/index.js +433 -0
- package/dist/react/index.d.ts +53 -0
- package/dist/react/index.js +406 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Klappay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @klappay/one
|
|
2
|
+
|
|
3
|
+
The embeddable payment button for Klappay One. Drop it on any page — it
|
|
4
|
+
opens a modal (an iframe, on desktop) or a popup (mobile) that handles
|
|
5
|
+
identity, wallet connection, and payment approval entirely on Klappay's
|
|
6
|
+
own origin, then reports the result back to you.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @klappay/one
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or via `<script>`, no build step required:
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<script src="https://js.klappay.com/one@1.js"></script>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Pin to a specific version instead of the `@1` major alias if you need a
|
|
21
|
+
frozen build — an exact version is also required if you want to add
|
|
22
|
+
`integrity`/`crossorigin` for Subresource Integrity, since the `@1` alias
|
|
23
|
+
is a moving target and can't carry a fixed hash.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
### Drop-in button
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<klappay-button charge-id="ch_123" variant="black" size="md"></klappay-button>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Your own button
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<button data-klappay-one="ch_123">Pay with Klappay</button>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Both are wired up automatically once the script loads — no JavaScript
|
|
40
|
+
required for either. Both also accept `mode="iframe"` / `mode="popup"`
|
|
41
|
+
(`data-klappay-one-mode` for the plain button) to force a mode instead of
|
|
42
|
+
the mobile/desktop default.
|
|
43
|
+
|
|
44
|
+
### Programmatic
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { createKlappayOne } from '@klappay/one'
|
|
48
|
+
|
|
49
|
+
const klappayOne = createKlappayOne({
|
|
50
|
+
chargeId: 'ch_123',
|
|
51
|
+
onSuccess: (result) => {
|
|
52
|
+
// UX signal only — confirm fulfillment via Klappay Core's webhook,
|
|
53
|
+
// never from this callback alone.
|
|
54
|
+
},
|
|
55
|
+
onError: (error) => console.error(error),
|
|
56
|
+
onCancel: () => console.log('payer closed the checkout'),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
klappayOne.open()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Defaults to an iframe/modal on desktop and a popup on mobile — pass
|
|
63
|
+
`mode: 'iframe' | 'popup'` to force one or the other.
|
|
64
|
+
|
|
65
|
+
### React
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { KlappayButton } from '@klappay/one/react'
|
|
69
|
+
|
|
70
|
+
function Checkout() {
|
|
71
|
+
return <KlappayButton chargeId="ch_123" variant="black" size="md" />
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
See `ONE_JS_SDK.md` and `ONE_ID.md` in the `klap-one` repository for the
|
|
78
|
+
full protocol and security model this package implements against.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
interface KlappayOneError {
|
|
2
|
+
code: string;
|
|
3
|
+
message: string;
|
|
4
|
+
}
|
|
5
|
+
interface PaymentResult {
|
|
6
|
+
txHash: string;
|
|
7
|
+
walletAddress: string;
|
|
8
|
+
network: string;
|
|
9
|
+
amount: string;
|
|
10
|
+
confirmedAt: string;
|
|
11
|
+
}
|
|
12
|
+
interface KlappayOneConfig {
|
|
13
|
+
chargeId: string;
|
|
14
|
+
origin: string;
|
|
15
|
+
locale?: string;
|
|
16
|
+
mode?: 'iframe' | 'popup';
|
|
17
|
+
onReady?: () => void;
|
|
18
|
+
onSuccess?: (result: PaymentResult) => void;
|
|
19
|
+
onError?: (error: KlappayOneError) => void;
|
|
20
|
+
onCancel?: () => void;
|
|
21
|
+
}
|
|
22
|
+
interface KlappayOne {
|
|
23
|
+
open: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface GlobalConfig {
|
|
27
|
+
origin?: string;
|
|
28
|
+
locale?: string;
|
|
29
|
+
}
|
|
30
|
+
declare function configure(config: GlobalConfig): void;
|
|
31
|
+
declare function createKlappayOne(config: KlappayOneConfig): KlappayOne;
|
|
32
|
+
|
|
33
|
+
export { type KlappayOne, type KlappayOneConfig, type KlappayOneError, type PaymentResult, configure, createKlappayOne };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";var KlappayOne=(()=>{var f=Object.defineProperty;var z=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var B=Object.prototype.hasOwnProperty;var L=(e,t)=>{for(var o in t)f(e,o,{get:t[o],enumerable:!0})},H=(e,t,o,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of M(t))!B.call(e,n)&&n!==o&&f(e,n,{get:()=>t[n],enumerable:!(a=z(t,n))||a.enumerable});return e};var $=e=>H(f({},"__esModule",{value:!0}),e);var oe={};L(oe,{configure:()=>P,createKlappayOne:()=>u});function q(e){return typeof e=="object"&&e!==null&&"type"in e&&typeof e.type=="string"&&e.type.startsWith("klappay:")&&"requestId"in e&&typeof e.requestId=="string"}function m(e,t,o){let a=!1,n=setTimeout(()=>{a||o.onTimeout?.()},1e4);function i(r){if(r.origin!==e||!q(r.data)||r.data.requestId!==t)return;let s=r.data;switch(s.type){case"klappay:ready":a=!0,clearTimeout(n),o.onReady?.();return;case"klappay:success":o.onSuccess?.(s.result);return;case"klappay:error":o.onError?.(s.error);return;case"klappay:cancel":o.onCancel?.();return;case"klappay:resize":o.onResize?.(s.height);return}}return window.addEventListener("message",i),()=>{clearTimeout(n),window.removeEventListener("message",i)}}function h(e){let t=new URL("/id/",e.klapOneOrigin);return t.searchParams.set("chargeId",e.chargeId),t.searchParams.set("requestId",e.requestId),t.searchParams.set("returnOrigin",window.location.origin),e.locale&&t.searchParams.set("locale",e.locale),t.toString()}var D=/Android|iPhone|iPad|iPod/i;function T(e){return D.test(e)}function N(){return`
|
|
2
|
+
.backdrop {
|
|
3
|
+
position: fixed;
|
|
4
|
+
inset: 0;
|
|
5
|
+
background: rgba(0, 0, 0, 0.5);
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
z-index: 2147483647;
|
|
10
|
+
}
|
|
11
|
+
.frame {
|
|
12
|
+
width: 420px;
|
|
13
|
+
max-width: calc(100vw - 32px);
|
|
14
|
+
height: 720px;
|
|
15
|
+
max-height: calc(100vh - 32px);
|
|
16
|
+
border: none;
|
|
17
|
+
border-radius: 12px;
|
|
18
|
+
background: #000;
|
|
19
|
+
transition: height 0.15s ease;
|
|
20
|
+
}
|
|
21
|
+
`}function O(e,t){let o=document.createElement("div"),a=o.attachShadow({mode:"open"}),n=document.createElement("style");n.textContent=N();let i=document.createElement("div");i.className="backdrop",i.addEventListener("click",s=>{s.target===i&&t()});let r=document.createElement("iframe");return r.className="frame",r.src=e,i.append(r),a.append(n,i),document.body.append(o),{close:()=>o.remove(),resize:s=>{r.style.height=`${s}px`}}}var G="klappay-one";function k(e){let t=window.screenX+(window.outerWidth-420)/2,o=window.screenY+(window.outerHeight-720)/2;return window.open(e,G,`width=420,height=720,left=${t},top=${o}`)}function b(e){return e===null||e.closed}var W={code:"POPUP_BLOCKED",message:"The Klappay One popup was blocked."},V={code:"FRAME_TIMEOUT",message:"The Klappay One popup did not respond in time."},F=500,x={};function P(e){x=e}function l(){return x}function Y(e){return e.mode??(T(navigator.userAgent)?"popup":"iframe")}function u(e){function t(){Y(e)==="popup"?o():a()}function o(){let n=crypto.randomUUID(),i=h({klapOneOrigin:e.origin,chargeId:e.chargeId,requestId:n,locale:e.locale}),r=k(i);if(b(r)){e.onError?.(W);return}let s=m(e.origin,n,{onReady:e.onReady,onSuccess:d=>{p(),e.onSuccess?.(d)},onError:d=>{p(),e.onError?.(d)},onCancel:()=>{p(),e.onCancel?.()},onTimeout:()=>{p(),e.onError?.(V)}}),y=setInterval(()=>{b(r)&&(p(),e.onCancel?.())},F);function p(){clearInterval(y),s()}}function a(){let n=crypto.randomUUID(),i=h({klapOneOrigin:e.origin,chargeId:e.chargeId,requestId:n,locale:e.locale}),r=!1;function s(c){r||(r=!0,d(),c())}let y=O(i,()=>s(()=>e.onCancel?.())),p=m(e.origin,n,{onReady:e.onReady,onSuccess:c=>s(()=>e.onSuccess?.(c)),onError:c=>s(()=>e.onError?.(c)),onCancel:()=>s(()=>e.onCancel?.()),onResize:c=>y.resize(c),onTimeout:()=>s(o)});function d(){p(),y.close()}}return{open:t}}var g="data-klappay-one",w="data-klappay-one-origin",j="data-klappay-one-locale",Z="data-klappay-one-mode",_="data-klappay-one-wired",E="data-klappay-one-busy";function v(e){e.hasAttribute(_)||(e.setAttribute(_,""),e.addEventListener("click",()=>{if(e.hasAttribute(E))return;let t=e.getAttribute(g);if(!t)return;let o=e.getAttribute(w)??l().origin;if(!o){console.error(`[${g}] has no origin \u2014 set ${w} or call KlappayOne.configure({ origin }).`);return}let a=e.getAttribute(j)??l().locale,n=e.getAttribute(Z);e.setAttribute(E,"");let i=()=>{e.removeAttribute(E)};u({chargeId:t,origin:o,locale:a,mode:n==="iframe"||n==="popup"?n:void 0,onSuccess:r=>{i(),e.dispatchEvent(new CustomEvent("success",{detail:r}))},onError:r=>{i(),e.dispatchEvent(new CustomEvent("error",{detail:r}))},onCancel:()=>{i(),e.dispatchEvent(new CustomEvent("cancel"))}}).open()}))}function I(e=document){for(let t of e.querySelectorAll(`[${g}]`))v(t)}function R(e=document.body){let t=new MutationObserver(o=>{for(let a of o)for(let n of a.addedNodes)if(n instanceof Element){n.hasAttribute(g)&&v(n);for(let i of n.querySelectorAll(`[${g}]`))v(i)}});return t.observe(e,{childList:!0,subtree:!0}),()=>t.disconnect()}var C="klappay-button",S=["white","yellow","black"],K=["sm","md","lg"],X="black",J="md",Q={sm:{height:"32px",fontSize:"13px",padding:"0 14px"},md:{height:"40px",fontSize:"14px",padding:"0 18px"},lg:{height:"48px",fontSize:"16px",padding:"0 24px"}},ee={white:{background:"#ffffff",color:"#111111",border:"1px solid #e5e5e5"},yellow:{background:"#f2b90c",color:"#111111",border:"none"},black:{background:"#111111",color:"#ffffff",border:"none"}};function te(e){return S.includes(e)}function ne(e){return K.includes(e)}var A=class e extends HTMLElement{static get observedAttributes(){return["variant","size"]}#e;constructor(){super();let t=this.attachShadow({mode:"open"}),o=document.createElement("style");o.textContent=e.#r(),this.#e=document.createElement("button"),this.#e.type="button",this.#e.textContent="Pay with Klappay",this.#e.addEventListener("click",()=>this.#o()),t.append(o,this.#e),this.#t(),this.#n()}attributeChangedCallback(t){t==="variant"&&this.#t(),t==="size"&&this.#n()}get variant(){let t=this.getAttribute("variant")??"";return te(t)?t:X}get size(){let t=this.getAttribute("size")??"";return ne(t)?t:J}#t(){this.#e.setAttribute("data-variant",this.variant)}#n(){this.#e.setAttribute("data-size",this.size)}#o(){if(this.#e.disabled)return;let t=this.getAttribute("charge-id");if(!t){console.error("<klappay-button> is missing a required charge-id attribute.");return}let o=this.getAttribute("origin")??l().origin;if(!o){console.error("<klappay-button> has no origin \u2014 set the origin attribute or call KlappayOne.configure({ origin }).");return}let a=this.getAttribute("locale")??l().locale,n=this.getAttribute("mode");this.#e.disabled=!0;let i=()=>{this.#e.disabled=!1};u({chargeId:t,origin:o,locale:a,mode:n==="iframe"||n==="popup"?n:void 0,onSuccess:r=>{i(),this.dispatchEvent(new CustomEvent("success",{detail:r}))},onError:r=>{i(),this.dispatchEvent(new CustomEvent("error",{detail:r}))},onCancel:()=>{i(),this.dispatchEvent(new CustomEvent("cancel"))}}).open()}static#r(){let t=S.map(a=>{let{background:n,color:i,border:r}=ee[a];return`button[data-variant="${a}"] { background: var(--klappay-background, ${n}); color: var(--klappay-color, ${i}); border: ${r}; }`}).join(`
|
|
22
|
+
`),o=K.map(a=>{let{height:n,fontSize:i,padding:r}=Q[a];return`button[data-size="${a}"] { height: var(--klappay-button-height, ${n}); font-size: ${i}; padding: ${r}; }`}).join(`
|
|
23
|
+
`);return`
|
|
24
|
+
button {
|
|
25
|
+
display: inline-flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
border-radius: var(--klappay-radius, 8px);
|
|
29
|
+
font-family: var(--klappay-font-family, system-ui, sans-serif);
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
transition: opacity 0.15s ease;
|
|
33
|
+
}
|
|
34
|
+
button:hover { opacity: 0.9; }
|
|
35
|
+
button:active { opacity: 0.8; }
|
|
36
|
+
button:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
37
|
+
${t}
|
|
38
|
+
${o}
|
|
39
|
+
`}};function U(){customElements.get(C)||customElements.define(C,A)}U();document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>I()):I();R();return $(oe);})();
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// src/core/bridge.ts
|
|
2
|
+
var READY_TIMEOUT_MS = 1e4;
|
|
3
|
+
function isBridgeMessage(data) {
|
|
4
|
+
return typeof data === "object" && data !== null && "type" in data && typeof data.type === "string" && data.type.startsWith("klappay:") && "requestId" in data && typeof data.requestId === "string";
|
|
5
|
+
}
|
|
6
|
+
function listen(klapOneOrigin, requestId, handlers) {
|
|
7
|
+
let ready = false;
|
|
8
|
+
const timeout = setTimeout(() => {
|
|
9
|
+
if (!ready) handlers.onTimeout?.();
|
|
10
|
+
}, READY_TIMEOUT_MS);
|
|
11
|
+
function onMessage(event) {
|
|
12
|
+
if (event.origin !== klapOneOrigin) return;
|
|
13
|
+
if (!isBridgeMessage(event.data) || event.data.requestId !== requestId) return;
|
|
14
|
+
const message = event.data;
|
|
15
|
+
switch (message.type) {
|
|
16
|
+
case "klappay:ready":
|
|
17
|
+
ready = true;
|
|
18
|
+
clearTimeout(timeout);
|
|
19
|
+
handlers.onReady?.();
|
|
20
|
+
return;
|
|
21
|
+
case "klappay:success":
|
|
22
|
+
handlers.onSuccess?.(message.result);
|
|
23
|
+
return;
|
|
24
|
+
case "klappay:error":
|
|
25
|
+
handlers.onError?.(message.error);
|
|
26
|
+
return;
|
|
27
|
+
case "klappay:cancel":
|
|
28
|
+
handlers.onCancel?.();
|
|
29
|
+
return;
|
|
30
|
+
case "klappay:resize":
|
|
31
|
+
handlers.onResize?.(message.height);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
window.addEventListener("message", onMessage);
|
|
36
|
+
return () => {
|
|
37
|
+
clearTimeout(timeout);
|
|
38
|
+
window.removeEventListener("message", onMessage);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function buildFrameUrl(params) {
|
|
42
|
+
const url = new URL("/id/", params.klapOneOrigin);
|
|
43
|
+
url.searchParams.set("chargeId", params.chargeId);
|
|
44
|
+
url.searchParams.set("requestId", params.requestId);
|
|
45
|
+
url.searchParams.set("returnOrigin", window.location.origin);
|
|
46
|
+
if (params.locale) url.searchParams.set("locale", params.locale);
|
|
47
|
+
return url.toString();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/core/device.ts
|
|
51
|
+
var MOBILE_USER_AGENT_PATTERN = /Android|iPhone|iPad|iPod/i;
|
|
52
|
+
function isMobileUserAgent(userAgent) {
|
|
53
|
+
return MOBILE_USER_AGENT_PATTERN.test(userAgent);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/core/iframe.ts
|
|
57
|
+
var FRAME_WIDTH = 420;
|
|
58
|
+
var INITIAL_FRAME_HEIGHT = 720;
|
|
59
|
+
function css() {
|
|
60
|
+
return `
|
|
61
|
+
.backdrop {
|
|
62
|
+
position: fixed;
|
|
63
|
+
inset: 0;
|
|
64
|
+
background: rgba(0, 0, 0, 0.5);
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
z-index: 2147483647;
|
|
69
|
+
}
|
|
70
|
+
.frame {
|
|
71
|
+
width: ${FRAME_WIDTH}px;
|
|
72
|
+
max-width: calc(100vw - 32px);
|
|
73
|
+
height: ${INITIAL_FRAME_HEIGHT}px;
|
|
74
|
+
max-height: calc(100vh - 32px);
|
|
75
|
+
border: none;
|
|
76
|
+
border-radius: 12px;
|
|
77
|
+
background: #000;
|
|
78
|
+
transition: height 0.15s ease;
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
function openIframe(url, onDismiss) {
|
|
83
|
+
const host = document.createElement("div");
|
|
84
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
85
|
+
const style = document.createElement("style");
|
|
86
|
+
style.textContent = css();
|
|
87
|
+
const backdrop = document.createElement("div");
|
|
88
|
+
backdrop.className = "backdrop";
|
|
89
|
+
backdrop.addEventListener("click", (event) => {
|
|
90
|
+
if (event.target === backdrop) onDismiss();
|
|
91
|
+
});
|
|
92
|
+
const frame = document.createElement("iframe");
|
|
93
|
+
frame.className = "frame";
|
|
94
|
+
frame.src = url;
|
|
95
|
+
backdrop.append(frame);
|
|
96
|
+
shadow.append(style, backdrop);
|
|
97
|
+
document.body.append(host);
|
|
98
|
+
return {
|
|
99
|
+
close: () => host.remove(),
|
|
100
|
+
resize: (height) => {
|
|
101
|
+
frame.style.height = `${height}px`;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/core/popup.ts
|
|
107
|
+
var POPUP_WIDTH = 420;
|
|
108
|
+
var POPUP_HEIGHT = 720;
|
|
109
|
+
var POPUP_NAME = "klappay-one";
|
|
110
|
+
function openPopup(url) {
|
|
111
|
+
const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;
|
|
112
|
+
const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;
|
|
113
|
+
return window.open(
|
|
114
|
+
url,
|
|
115
|
+
POPUP_NAME,
|
|
116
|
+
`width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
function isPopupClosed(popup) {
|
|
120
|
+
return popup === null || popup.closed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/core/klappay-one.ts
|
|
124
|
+
var POPUP_BLOCKED = { code: "POPUP_BLOCKED", message: "The Klappay One popup was blocked." };
|
|
125
|
+
var FRAME_TIMEOUT = {
|
|
126
|
+
code: "FRAME_TIMEOUT",
|
|
127
|
+
message: "The Klappay One popup did not respond in time."
|
|
128
|
+
};
|
|
129
|
+
var POPUP_CLOSED_POLL_MS = 500;
|
|
130
|
+
var globalConfig = {};
|
|
131
|
+
function configure(config) {
|
|
132
|
+
globalConfig = config;
|
|
133
|
+
}
|
|
134
|
+
function getGlobalConfig() {
|
|
135
|
+
return globalConfig;
|
|
136
|
+
}
|
|
137
|
+
function resolveMode(config) {
|
|
138
|
+
return config.mode ?? (isMobileUserAgent(navigator.userAgent) ? "popup" : "iframe");
|
|
139
|
+
}
|
|
140
|
+
function createKlappayOne(config) {
|
|
141
|
+
function open() {
|
|
142
|
+
if (resolveMode(config) === "popup") {
|
|
143
|
+
openViaPopup();
|
|
144
|
+
} else {
|
|
145
|
+
openViaIframe();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function openViaPopup() {
|
|
149
|
+
const requestId = crypto.randomUUID();
|
|
150
|
+
const url = buildFrameUrl({
|
|
151
|
+
klapOneOrigin: config.origin,
|
|
152
|
+
chargeId: config.chargeId,
|
|
153
|
+
requestId,
|
|
154
|
+
locale: config.locale
|
|
155
|
+
});
|
|
156
|
+
const popup = openPopup(url);
|
|
157
|
+
if (isPopupClosed(popup)) {
|
|
158
|
+
config.onError?.(POPUP_BLOCKED);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const stopListening = listen(config.origin, requestId, {
|
|
162
|
+
onReady: config.onReady,
|
|
163
|
+
onSuccess: (result) => {
|
|
164
|
+
stop();
|
|
165
|
+
config.onSuccess?.(result);
|
|
166
|
+
},
|
|
167
|
+
onError: (error) => {
|
|
168
|
+
stop();
|
|
169
|
+
config.onError?.(error);
|
|
170
|
+
},
|
|
171
|
+
onCancel: () => {
|
|
172
|
+
stop();
|
|
173
|
+
config.onCancel?.();
|
|
174
|
+
},
|
|
175
|
+
onTimeout: () => {
|
|
176
|
+
stop();
|
|
177
|
+
config.onError?.(FRAME_TIMEOUT);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
const pollClosed = setInterval(() => {
|
|
181
|
+
if (isPopupClosed(popup)) {
|
|
182
|
+
stop();
|
|
183
|
+
config.onCancel?.();
|
|
184
|
+
}
|
|
185
|
+
}, POPUP_CLOSED_POLL_MS);
|
|
186
|
+
function stop() {
|
|
187
|
+
clearInterval(pollClosed);
|
|
188
|
+
stopListening();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function openViaIframe() {
|
|
192
|
+
const requestId = crypto.randomUUID();
|
|
193
|
+
const url = buildFrameUrl({
|
|
194
|
+
klapOneOrigin: config.origin,
|
|
195
|
+
chargeId: config.chargeId,
|
|
196
|
+
requestId,
|
|
197
|
+
locale: config.locale
|
|
198
|
+
});
|
|
199
|
+
let settled = false;
|
|
200
|
+
function settleOnce(run) {
|
|
201
|
+
if (settled) return;
|
|
202
|
+
settled = true;
|
|
203
|
+
stop();
|
|
204
|
+
run();
|
|
205
|
+
}
|
|
206
|
+
const frame = openIframe(url, () => settleOnce(() => config.onCancel?.()));
|
|
207
|
+
const stopListening = listen(config.origin, requestId, {
|
|
208
|
+
onReady: config.onReady,
|
|
209
|
+
onSuccess: (result) => settleOnce(() => config.onSuccess?.(result)),
|
|
210
|
+
onError: (error) => settleOnce(() => config.onError?.(error)),
|
|
211
|
+
onCancel: () => settleOnce(() => config.onCancel?.()),
|
|
212
|
+
onResize: (height) => frame.resize(height),
|
|
213
|
+
// A merchant CSP blocking frame-src/child-src for this domain would
|
|
214
|
+
// otherwise leave the iframe silently blank forever — one automatic
|
|
215
|
+
// fallback to the popup renderer before giving up and reporting
|
|
216
|
+
// FRAME_TIMEOUT (which openViaPopup does on its own if it also fails).
|
|
217
|
+
onTimeout: () => settleOnce(openViaPopup)
|
|
218
|
+
});
|
|
219
|
+
function stop() {
|
|
220
|
+
stopListening();
|
|
221
|
+
frame.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { open };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/ui/auto-wire.ts
|
|
228
|
+
var AUTO_WIRE_ATTRIBUTE = "data-klappay-one";
|
|
229
|
+
var AUTO_WIRE_ORIGIN_ATTRIBUTE = "data-klappay-one-origin";
|
|
230
|
+
var AUTO_WIRE_LOCALE_ATTRIBUTE = "data-klappay-one-locale";
|
|
231
|
+
var AUTO_WIRE_MODE_ATTRIBUTE = "data-klappay-one-mode";
|
|
232
|
+
var WIRED_MARKER = "data-klappay-one-wired";
|
|
233
|
+
var BUSY_MARKER = "data-klappay-one-busy";
|
|
234
|
+
function wire(element) {
|
|
235
|
+
if (element.hasAttribute(WIRED_MARKER)) return;
|
|
236
|
+
element.setAttribute(WIRED_MARKER, "");
|
|
237
|
+
element.addEventListener("click", () => {
|
|
238
|
+
if (element.hasAttribute(BUSY_MARKER)) return;
|
|
239
|
+
const chargeId = element.getAttribute(AUTO_WIRE_ATTRIBUTE);
|
|
240
|
+
if (!chargeId) return;
|
|
241
|
+
const origin = element.getAttribute(AUTO_WIRE_ORIGIN_ATTRIBUTE) ?? getGlobalConfig().origin;
|
|
242
|
+
if (!origin) {
|
|
243
|
+
console.error(
|
|
244
|
+
`[${AUTO_WIRE_ATTRIBUTE}] has no origin \u2014 set ${AUTO_WIRE_ORIGIN_ATTRIBUTE} or call KlappayOne.configure({ origin }).`
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const locale = element.getAttribute(AUTO_WIRE_LOCALE_ATTRIBUTE) ?? getGlobalConfig().locale;
|
|
249
|
+
const mode = element.getAttribute(AUTO_WIRE_MODE_ATTRIBUTE);
|
|
250
|
+
element.setAttribute(BUSY_MARKER, "");
|
|
251
|
+
const clearBusy = () => {
|
|
252
|
+
element.removeAttribute(BUSY_MARKER);
|
|
253
|
+
};
|
|
254
|
+
createKlappayOne({
|
|
255
|
+
chargeId,
|
|
256
|
+
origin,
|
|
257
|
+
locale,
|
|
258
|
+
mode: mode === "iframe" || mode === "popup" ? mode : void 0,
|
|
259
|
+
onSuccess: (result) => {
|
|
260
|
+
clearBusy();
|
|
261
|
+
element.dispatchEvent(new CustomEvent("success", { detail: result }));
|
|
262
|
+
},
|
|
263
|
+
onError: (error) => {
|
|
264
|
+
clearBusy();
|
|
265
|
+
element.dispatchEvent(new CustomEvent("error", { detail: error }));
|
|
266
|
+
},
|
|
267
|
+
onCancel: () => {
|
|
268
|
+
clearBusy();
|
|
269
|
+
element.dispatchEvent(new CustomEvent("cancel"));
|
|
270
|
+
}
|
|
271
|
+
}).open();
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function wireExisting(root = document) {
|
|
275
|
+
for (const element of root.querySelectorAll(`[${AUTO_WIRE_ATTRIBUTE}]`)) wire(element);
|
|
276
|
+
}
|
|
277
|
+
function observeNewElements(root = document.body) {
|
|
278
|
+
const observer = new MutationObserver((mutations) => {
|
|
279
|
+
for (const mutation of mutations) {
|
|
280
|
+
for (const node of mutation.addedNodes) {
|
|
281
|
+
if (!(node instanceof Element)) continue;
|
|
282
|
+
if (node.hasAttribute(AUTO_WIRE_ATTRIBUTE)) wire(node);
|
|
283
|
+
for (const element of node.querySelectorAll(`[${AUTO_WIRE_ATTRIBUTE}]`)) wire(element);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
288
|
+
return () => observer.disconnect();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/ui/klappay-button.ts
|
|
292
|
+
var KLAPPAY_BUTTON_TAG = "klappay-button";
|
|
293
|
+
var VARIANTS = ["white", "yellow", "black"];
|
|
294
|
+
var SIZES = ["sm", "md", "lg"];
|
|
295
|
+
var DEFAULT_VARIANT = "black";
|
|
296
|
+
var DEFAULT_SIZE = "md";
|
|
297
|
+
var SIZE_STYLES = {
|
|
298
|
+
sm: { height: "32px", fontSize: "13px", padding: "0 14px" },
|
|
299
|
+
md: { height: "40px", fontSize: "14px", padding: "0 18px" },
|
|
300
|
+
lg: { height: "48px", fontSize: "16px", padding: "0 24px" }
|
|
301
|
+
};
|
|
302
|
+
var VARIANT_STYLES = {
|
|
303
|
+
white: { background: "#ffffff", color: "#111111", border: "1px solid #e5e5e5" },
|
|
304
|
+
yellow: { background: "#f2b90c", color: "#111111", border: "none" },
|
|
305
|
+
black: { background: "#111111", color: "#ffffff", border: "none" }
|
|
306
|
+
};
|
|
307
|
+
function isVariant(value) {
|
|
308
|
+
return VARIANTS.includes(value);
|
|
309
|
+
}
|
|
310
|
+
function isSize(value) {
|
|
311
|
+
return SIZES.includes(value);
|
|
312
|
+
}
|
|
313
|
+
var KlappayButtonElement = class _KlappayButtonElement extends HTMLElement {
|
|
314
|
+
static get observedAttributes() {
|
|
315
|
+
return ["variant", "size"];
|
|
316
|
+
}
|
|
317
|
+
#button;
|
|
318
|
+
constructor() {
|
|
319
|
+
super();
|
|
320
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
321
|
+
const style = document.createElement("style");
|
|
322
|
+
style.textContent = _KlappayButtonElement.#css();
|
|
323
|
+
this.#button = document.createElement("button");
|
|
324
|
+
this.#button.type = "button";
|
|
325
|
+
this.#button.textContent = "Pay with Klappay";
|
|
326
|
+
this.#button.addEventListener("click", () => this.#handleClick());
|
|
327
|
+
shadow.append(style, this.#button);
|
|
328
|
+
this.#applyVariant();
|
|
329
|
+
this.#applySize();
|
|
330
|
+
}
|
|
331
|
+
attributeChangedCallback(name) {
|
|
332
|
+
if (name === "variant") this.#applyVariant();
|
|
333
|
+
if (name === "size") this.#applySize();
|
|
334
|
+
}
|
|
335
|
+
get variant() {
|
|
336
|
+
const value = this.getAttribute("variant") ?? "";
|
|
337
|
+
return isVariant(value) ? value : DEFAULT_VARIANT;
|
|
338
|
+
}
|
|
339
|
+
get size() {
|
|
340
|
+
const value = this.getAttribute("size") ?? "";
|
|
341
|
+
return isSize(value) ? value : DEFAULT_SIZE;
|
|
342
|
+
}
|
|
343
|
+
#applyVariant() {
|
|
344
|
+
this.#button.setAttribute("data-variant", this.variant);
|
|
345
|
+
}
|
|
346
|
+
#applySize() {
|
|
347
|
+
this.#button.setAttribute("data-size", this.size);
|
|
348
|
+
}
|
|
349
|
+
#handleClick() {
|
|
350
|
+
if (this.#button.disabled) return;
|
|
351
|
+
const chargeId = this.getAttribute("charge-id");
|
|
352
|
+
if (!chargeId) {
|
|
353
|
+
console.error("<klappay-button> is missing a required charge-id attribute.");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const origin = this.getAttribute("origin") ?? getGlobalConfig().origin;
|
|
357
|
+
if (!origin) {
|
|
358
|
+
console.error(
|
|
359
|
+
"<klappay-button> has no origin \u2014 set the origin attribute or call KlappayOne.configure({ origin })."
|
|
360
|
+
);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const locale = this.getAttribute("locale") ?? getGlobalConfig().locale;
|
|
364
|
+
const mode = this.getAttribute("mode");
|
|
365
|
+
this.#button.disabled = true;
|
|
366
|
+
const reenable = () => {
|
|
367
|
+
this.#button.disabled = false;
|
|
368
|
+
};
|
|
369
|
+
createKlappayOne({
|
|
370
|
+
chargeId,
|
|
371
|
+
origin,
|
|
372
|
+
locale,
|
|
373
|
+
mode: mode === "iframe" || mode === "popup" ? mode : void 0,
|
|
374
|
+
onSuccess: (result) => {
|
|
375
|
+
reenable();
|
|
376
|
+
this.dispatchEvent(new CustomEvent("success", { detail: result }));
|
|
377
|
+
},
|
|
378
|
+
onError: (error) => {
|
|
379
|
+
reenable();
|
|
380
|
+
this.dispatchEvent(new CustomEvent("error", { detail: error }));
|
|
381
|
+
},
|
|
382
|
+
onCancel: () => {
|
|
383
|
+
reenable();
|
|
384
|
+
this.dispatchEvent(new CustomEvent("cancel"));
|
|
385
|
+
}
|
|
386
|
+
}).open();
|
|
387
|
+
}
|
|
388
|
+
static #css() {
|
|
389
|
+
const variantRules = VARIANTS.map((variant) => {
|
|
390
|
+
const { background, color, border } = VARIANT_STYLES[variant];
|
|
391
|
+
return `button[data-variant="${variant}"] { background: var(--klappay-background, ${background}); color: var(--klappay-color, ${color}); border: ${border}; }`;
|
|
392
|
+
}).join("\n");
|
|
393
|
+
const sizeRules = SIZES.map((size) => {
|
|
394
|
+
const { height, fontSize, padding } = SIZE_STYLES[size];
|
|
395
|
+
return `button[data-size="${size}"] { height: var(--klappay-button-height, ${height}); font-size: ${fontSize}; padding: ${padding}; }`;
|
|
396
|
+
}).join("\n");
|
|
397
|
+
return `
|
|
398
|
+
button {
|
|
399
|
+
display: inline-flex;
|
|
400
|
+
align-items: center;
|
|
401
|
+
justify-content: center;
|
|
402
|
+
border-radius: var(--klappay-radius, 8px);
|
|
403
|
+
font-family: var(--klappay-font-family, system-ui, sans-serif);
|
|
404
|
+
font-weight: 600;
|
|
405
|
+
cursor: pointer;
|
|
406
|
+
transition: opacity 0.15s ease;
|
|
407
|
+
}
|
|
408
|
+
button:hover { opacity: 0.9; }
|
|
409
|
+
button:active { opacity: 0.8; }
|
|
410
|
+
button:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
411
|
+
${variantRules}
|
|
412
|
+
${sizeRules}
|
|
413
|
+
`;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
function registerKlappayButton() {
|
|
417
|
+
if (!customElements.get(KLAPPAY_BUTTON_TAG)) {
|
|
418
|
+
customElements.define(KLAPPAY_BUTTON_TAG, KlappayButtonElement);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/index.ts
|
|
423
|
+
registerKlappayButton();
|
|
424
|
+
if (document.readyState === "loading") {
|
|
425
|
+
document.addEventListener("DOMContentLoaded", () => wireExisting());
|
|
426
|
+
} else {
|
|
427
|
+
wireExisting();
|
|
428
|
+
}
|
|
429
|
+
observeNewElements();
|
|
430
|
+
export {
|
|
431
|
+
configure,
|
|
432
|
+
createKlappayOne
|
|
433
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
type KlappayButtonVariant = 'white' | 'yellow' | 'black';
|
|
2
|
+
type KlappayButtonSize = 'sm' | 'md' | 'lg';
|
|
3
|
+
interface KlappayOneError {
|
|
4
|
+
code: string;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
interface PaymentResult {
|
|
8
|
+
txHash: string;
|
|
9
|
+
walletAddress: string;
|
|
10
|
+
network: string;
|
|
11
|
+
amount: string;
|
|
12
|
+
confirmedAt: string;
|
|
13
|
+
}
|
|
14
|
+
interface KlappayOneConfig {
|
|
15
|
+
chargeId: string;
|
|
16
|
+
origin: string;
|
|
17
|
+
locale?: string;
|
|
18
|
+
mode?: 'iframe' | 'popup';
|
|
19
|
+
onReady?: () => void;
|
|
20
|
+
onSuccess?: (result: PaymentResult) => void;
|
|
21
|
+
onError?: (error: KlappayOneError) => void;
|
|
22
|
+
onCancel?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare function useKlappayOne(config: KlappayOneConfig): {
|
|
26
|
+
open: () => void;
|
|
27
|
+
};
|
|
28
|
+
interface KlappayButtonProps {
|
|
29
|
+
chargeId: string;
|
|
30
|
+
origin?: string;
|
|
31
|
+
locale?: string;
|
|
32
|
+
variant?: KlappayButtonVariant;
|
|
33
|
+
size?: KlappayButtonSize;
|
|
34
|
+
onSuccess?: (result: PaymentResult) => void;
|
|
35
|
+
onError?: (error: KlappayOneError) => void;
|
|
36
|
+
onCancel?: () => void;
|
|
37
|
+
}
|
|
38
|
+
declare global {
|
|
39
|
+
namespace JSX {
|
|
40
|
+
interface IntrinsicElements {
|
|
41
|
+
'klappay-button': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
|
|
42
|
+
'charge-id'?: string;
|
|
43
|
+
origin?: string;
|
|
44
|
+
locale?: string;
|
|
45
|
+
variant?: KlappayButtonVariant;
|
|
46
|
+
size?: KlappayButtonSize;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
declare function KlappayButton(props: KlappayButtonProps): JSX.Element;
|
|
52
|
+
|
|
53
|
+
export { KlappayButton, type KlappayButtonProps, useKlappayOne };
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// src/react/index.tsx
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
|
|
4
|
+
// src/core/bridge.ts
|
|
5
|
+
var READY_TIMEOUT_MS = 1e4;
|
|
6
|
+
function isBridgeMessage(data) {
|
|
7
|
+
return typeof data === "object" && data !== null && "type" in data && typeof data.type === "string" && data.type.startsWith("klappay:") && "requestId" in data && typeof data.requestId === "string";
|
|
8
|
+
}
|
|
9
|
+
function listen(klapOneOrigin, requestId, handlers) {
|
|
10
|
+
let ready = false;
|
|
11
|
+
const timeout = setTimeout(() => {
|
|
12
|
+
if (!ready) handlers.onTimeout?.();
|
|
13
|
+
}, READY_TIMEOUT_MS);
|
|
14
|
+
function onMessage(event) {
|
|
15
|
+
if (event.origin !== klapOneOrigin) return;
|
|
16
|
+
if (!isBridgeMessage(event.data) || event.data.requestId !== requestId) return;
|
|
17
|
+
const message = event.data;
|
|
18
|
+
switch (message.type) {
|
|
19
|
+
case "klappay:ready":
|
|
20
|
+
ready = true;
|
|
21
|
+
clearTimeout(timeout);
|
|
22
|
+
handlers.onReady?.();
|
|
23
|
+
return;
|
|
24
|
+
case "klappay:success":
|
|
25
|
+
handlers.onSuccess?.(message.result);
|
|
26
|
+
return;
|
|
27
|
+
case "klappay:error":
|
|
28
|
+
handlers.onError?.(message.error);
|
|
29
|
+
return;
|
|
30
|
+
case "klappay:cancel":
|
|
31
|
+
handlers.onCancel?.();
|
|
32
|
+
return;
|
|
33
|
+
case "klappay:resize":
|
|
34
|
+
handlers.onResize?.(message.height);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
window.addEventListener("message", onMessage);
|
|
39
|
+
return () => {
|
|
40
|
+
clearTimeout(timeout);
|
|
41
|
+
window.removeEventListener("message", onMessage);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function buildFrameUrl(params) {
|
|
45
|
+
const url = new URL("/id/", params.klapOneOrigin);
|
|
46
|
+
url.searchParams.set("chargeId", params.chargeId);
|
|
47
|
+
url.searchParams.set("requestId", params.requestId);
|
|
48
|
+
url.searchParams.set("returnOrigin", window.location.origin);
|
|
49
|
+
if (params.locale) url.searchParams.set("locale", params.locale);
|
|
50
|
+
return url.toString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/core/device.ts
|
|
54
|
+
var MOBILE_USER_AGENT_PATTERN = /Android|iPhone|iPad|iPod/i;
|
|
55
|
+
function isMobileUserAgent(userAgent) {
|
|
56
|
+
return MOBILE_USER_AGENT_PATTERN.test(userAgent);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/core/iframe.ts
|
|
60
|
+
var FRAME_WIDTH = 420;
|
|
61
|
+
var INITIAL_FRAME_HEIGHT = 720;
|
|
62
|
+
function css() {
|
|
63
|
+
return `
|
|
64
|
+
.backdrop {
|
|
65
|
+
position: fixed;
|
|
66
|
+
inset: 0;
|
|
67
|
+
background: rgba(0, 0, 0, 0.5);
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
z-index: 2147483647;
|
|
72
|
+
}
|
|
73
|
+
.frame {
|
|
74
|
+
width: ${FRAME_WIDTH}px;
|
|
75
|
+
max-width: calc(100vw - 32px);
|
|
76
|
+
height: ${INITIAL_FRAME_HEIGHT}px;
|
|
77
|
+
max-height: calc(100vh - 32px);
|
|
78
|
+
border: none;
|
|
79
|
+
border-radius: 12px;
|
|
80
|
+
background: #000;
|
|
81
|
+
transition: height 0.15s ease;
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
function openIframe(url, onDismiss) {
|
|
86
|
+
const host = document.createElement("div");
|
|
87
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
88
|
+
const style = document.createElement("style");
|
|
89
|
+
style.textContent = css();
|
|
90
|
+
const backdrop = document.createElement("div");
|
|
91
|
+
backdrop.className = "backdrop";
|
|
92
|
+
backdrop.addEventListener("click", (event) => {
|
|
93
|
+
if (event.target === backdrop) onDismiss();
|
|
94
|
+
});
|
|
95
|
+
const frame = document.createElement("iframe");
|
|
96
|
+
frame.className = "frame";
|
|
97
|
+
frame.src = url;
|
|
98
|
+
backdrop.append(frame);
|
|
99
|
+
shadow.append(style, backdrop);
|
|
100
|
+
document.body.append(host);
|
|
101
|
+
return {
|
|
102
|
+
close: () => host.remove(),
|
|
103
|
+
resize: (height) => {
|
|
104
|
+
frame.style.height = `${height}px`;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/core/popup.ts
|
|
110
|
+
var POPUP_WIDTH = 420;
|
|
111
|
+
var POPUP_HEIGHT = 720;
|
|
112
|
+
var POPUP_NAME = "klappay-one";
|
|
113
|
+
function openPopup(url) {
|
|
114
|
+
const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;
|
|
115
|
+
const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;
|
|
116
|
+
return window.open(
|
|
117
|
+
url,
|
|
118
|
+
POPUP_NAME,
|
|
119
|
+
`width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
function isPopupClosed(popup) {
|
|
123
|
+
return popup === null || popup.closed;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/core/klappay-one.ts
|
|
127
|
+
var POPUP_BLOCKED = { code: "POPUP_BLOCKED", message: "The Klappay One popup was blocked." };
|
|
128
|
+
var FRAME_TIMEOUT = {
|
|
129
|
+
code: "FRAME_TIMEOUT",
|
|
130
|
+
message: "The Klappay One popup did not respond in time."
|
|
131
|
+
};
|
|
132
|
+
var POPUP_CLOSED_POLL_MS = 500;
|
|
133
|
+
var globalConfig = {};
|
|
134
|
+
function getGlobalConfig() {
|
|
135
|
+
return globalConfig;
|
|
136
|
+
}
|
|
137
|
+
function resolveMode(config) {
|
|
138
|
+
return config.mode ?? (isMobileUserAgent(navigator.userAgent) ? "popup" : "iframe");
|
|
139
|
+
}
|
|
140
|
+
function createKlappayOne(config) {
|
|
141
|
+
function open() {
|
|
142
|
+
if (resolveMode(config) === "popup") {
|
|
143
|
+
openViaPopup();
|
|
144
|
+
} else {
|
|
145
|
+
openViaIframe();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function openViaPopup() {
|
|
149
|
+
const requestId = crypto.randomUUID();
|
|
150
|
+
const url = buildFrameUrl({
|
|
151
|
+
klapOneOrigin: config.origin,
|
|
152
|
+
chargeId: config.chargeId,
|
|
153
|
+
requestId,
|
|
154
|
+
locale: config.locale
|
|
155
|
+
});
|
|
156
|
+
const popup = openPopup(url);
|
|
157
|
+
if (isPopupClosed(popup)) {
|
|
158
|
+
config.onError?.(POPUP_BLOCKED);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const stopListening = listen(config.origin, requestId, {
|
|
162
|
+
onReady: config.onReady,
|
|
163
|
+
onSuccess: (result) => {
|
|
164
|
+
stop();
|
|
165
|
+
config.onSuccess?.(result);
|
|
166
|
+
},
|
|
167
|
+
onError: (error) => {
|
|
168
|
+
stop();
|
|
169
|
+
config.onError?.(error);
|
|
170
|
+
},
|
|
171
|
+
onCancel: () => {
|
|
172
|
+
stop();
|
|
173
|
+
config.onCancel?.();
|
|
174
|
+
},
|
|
175
|
+
onTimeout: () => {
|
|
176
|
+
stop();
|
|
177
|
+
config.onError?.(FRAME_TIMEOUT);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
const pollClosed = setInterval(() => {
|
|
181
|
+
if (isPopupClosed(popup)) {
|
|
182
|
+
stop();
|
|
183
|
+
config.onCancel?.();
|
|
184
|
+
}
|
|
185
|
+
}, POPUP_CLOSED_POLL_MS);
|
|
186
|
+
function stop() {
|
|
187
|
+
clearInterval(pollClosed);
|
|
188
|
+
stopListening();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function openViaIframe() {
|
|
192
|
+
const requestId = crypto.randomUUID();
|
|
193
|
+
const url = buildFrameUrl({
|
|
194
|
+
klapOneOrigin: config.origin,
|
|
195
|
+
chargeId: config.chargeId,
|
|
196
|
+
requestId,
|
|
197
|
+
locale: config.locale
|
|
198
|
+
});
|
|
199
|
+
let settled = false;
|
|
200
|
+
function settleOnce(run) {
|
|
201
|
+
if (settled) return;
|
|
202
|
+
settled = true;
|
|
203
|
+
stop();
|
|
204
|
+
run();
|
|
205
|
+
}
|
|
206
|
+
const frame = openIframe(url, () => settleOnce(() => config.onCancel?.()));
|
|
207
|
+
const stopListening = listen(config.origin, requestId, {
|
|
208
|
+
onReady: config.onReady,
|
|
209
|
+
onSuccess: (result) => settleOnce(() => config.onSuccess?.(result)),
|
|
210
|
+
onError: (error) => settleOnce(() => config.onError?.(error)),
|
|
211
|
+
onCancel: () => settleOnce(() => config.onCancel?.()),
|
|
212
|
+
onResize: (height) => frame.resize(height),
|
|
213
|
+
// A merchant CSP blocking frame-src/child-src for this domain would
|
|
214
|
+
// otherwise leave the iframe silently blank forever — one automatic
|
|
215
|
+
// fallback to the popup renderer before giving up and reporting
|
|
216
|
+
// FRAME_TIMEOUT (which openViaPopup does on its own if it also fails).
|
|
217
|
+
onTimeout: () => settleOnce(openViaPopup)
|
|
218
|
+
});
|
|
219
|
+
function stop() {
|
|
220
|
+
stopListening();
|
|
221
|
+
frame.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { open };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/ui/klappay-button.ts
|
|
228
|
+
var KLAPPAY_BUTTON_TAG = "klappay-button";
|
|
229
|
+
var VARIANTS = ["white", "yellow", "black"];
|
|
230
|
+
var SIZES = ["sm", "md", "lg"];
|
|
231
|
+
var DEFAULT_VARIANT = "black";
|
|
232
|
+
var DEFAULT_SIZE = "md";
|
|
233
|
+
var SIZE_STYLES = {
|
|
234
|
+
sm: { height: "32px", fontSize: "13px", padding: "0 14px" },
|
|
235
|
+
md: { height: "40px", fontSize: "14px", padding: "0 18px" },
|
|
236
|
+
lg: { height: "48px", fontSize: "16px", padding: "0 24px" }
|
|
237
|
+
};
|
|
238
|
+
var VARIANT_STYLES = {
|
|
239
|
+
white: { background: "#ffffff", color: "#111111", border: "1px solid #e5e5e5" },
|
|
240
|
+
yellow: { background: "#f2b90c", color: "#111111", border: "none" },
|
|
241
|
+
black: { background: "#111111", color: "#ffffff", border: "none" }
|
|
242
|
+
};
|
|
243
|
+
function isVariant(value) {
|
|
244
|
+
return VARIANTS.includes(value);
|
|
245
|
+
}
|
|
246
|
+
function isSize(value) {
|
|
247
|
+
return SIZES.includes(value);
|
|
248
|
+
}
|
|
249
|
+
var KlappayButtonElement = class _KlappayButtonElement extends HTMLElement {
|
|
250
|
+
static get observedAttributes() {
|
|
251
|
+
return ["variant", "size"];
|
|
252
|
+
}
|
|
253
|
+
#button;
|
|
254
|
+
constructor() {
|
|
255
|
+
super();
|
|
256
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
257
|
+
const style = document.createElement("style");
|
|
258
|
+
style.textContent = _KlappayButtonElement.#css();
|
|
259
|
+
this.#button = document.createElement("button");
|
|
260
|
+
this.#button.type = "button";
|
|
261
|
+
this.#button.textContent = "Pay with Klappay";
|
|
262
|
+
this.#button.addEventListener("click", () => this.#handleClick());
|
|
263
|
+
shadow.append(style, this.#button);
|
|
264
|
+
this.#applyVariant();
|
|
265
|
+
this.#applySize();
|
|
266
|
+
}
|
|
267
|
+
attributeChangedCallback(name) {
|
|
268
|
+
if (name === "variant") this.#applyVariant();
|
|
269
|
+
if (name === "size") this.#applySize();
|
|
270
|
+
}
|
|
271
|
+
get variant() {
|
|
272
|
+
const value = this.getAttribute("variant") ?? "";
|
|
273
|
+
return isVariant(value) ? value : DEFAULT_VARIANT;
|
|
274
|
+
}
|
|
275
|
+
get size() {
|
|
276
|
+
const value = this.getAttribute("size") ?? "";
|
|
277
|
+
return isSize(value) ? value : DEFAULT_SIZE;
|
|
278
|
+
}
|
|
279
|
+
#applyVariant() {
|
|
280
|
+
this.#button.setAttribute("data-variant", this.variant);
|
|
281
|
+
}
|
|
282
|
+
#applySize() {
|
|
283
|
+
this.#button.setAttribute("data-size", this.size);
|
|
284
|
+
}
|
|
285
|
+
#handleClick() {
|
|
286
|
+
if (this.#button.disabled) return;
|
|
287
|
+
const chargeId = this.getAttribute("charge-id");
|
|
288
|
+
if (!chargeId) {
|
|
289
|
+
console.error("<klappay-button> is missing a required charge-id attribute.");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const origin = this.getAttribute("origin") ?? getGlobalConfig().origin;
|
|
293
|
+
if (!origin) {
|
|
294
|
+
console.error(
|
|
295
|
+
"<klappay-button> has no origin \u2014 set the origin attribute or call KlappayOne.configure({ origin })."
|
|
296
|
+
);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const locale = this.getAttribute("locale") ?? getGlobalConfig().locale;
|
|
300
|
+
const mode = this.getAttribute("mode");
|
|
301
|
+
this.#button.disabled = true;
|
|
302
|
+
const reenable = () => {
|
|
303
|
+
this.#button.disabled = false;
|
|
304
|
+
};
|
|
305
|
+
createKlappayOne({
|
|
306
|
+
chargeId,
|
|
307
|
+
origin,
|
|
308
|
+
locale,
|
|
309
|
+
mode: mode === "iframe" || mode === "popup" ? mode : void 0,
|
|
310
|
+
onSuccess: (result) => {
|
|
311
|
+
reenable();
|
|
312
|
+
this.dispatchEvent(new CustomEvent("success", { detail: result }));
|
|
313
|
+
},
|
|
314
|
+
onError: (error) => {
|
|
315
|
+
reenable();
|
|
316
|
+
this.dispatchEvent(new CustomEvent("error", { detail: error }));
|
|
317
|
+
},
|
|
318
|
+
onCancel: () => {
|
|
319
|
+
reenable();
|
|
320
|
+
this.dispatchEvent(new CustomEvent("cancel"));
|
|
321
|
+
}
|
|
322
|
+
}).open();
|
|
323
|
+
}
|
|
324
|
+
static #css() {
|
|
325
|
+
const variantRules = VARIANTS.map((variant) => {
|
|
326
|
+
const { background, color, border } = VARIANT_STYLES[variant];
|
|
327
|
+
return `button[data-variant="${variant}"] { background: var(--klappay-background, ${background}); color: var(--klappay-color, ${color}); border: ${border}; }`;
|
|
328
|
+
}).join("\n");
|
|
329
|
+
const sizeRules = SIZES.map((size) => {
|
|
330
|
+
const { height, fontSize, padding } = SIZE_STYLES[size];
|
|
331
|
+
return `button[data-size="${size}"] { height: var(--klappay-button-height, ${height}); font-size: ${fontSize}; padding: ${padding}; }`;
|
|
332
|
+
}).join("\n");
|
|
333
|
+
return `
|
|
334
|
+
button {
|
|
335
|
+
display: inline-flex;
|
|
336
|
+
align-items: center;
|
|
337
|
+
justify-content: center;
|
|
338
|
+
border-radius: var(--klappay-radius, 8px);
|
|
339
|
+
font-family: var(--klappay-font-family, system-ui, sans-serif);
|
|
340
|
+
font-weight: 600;
|
|
341
|
+
cursor: pointer;
|
|
342
|
+
transition: opacity 0.15s ease;
|
|
343
|
+
}
|
|
344
|
+
button:hover { opacity: 0.9; }
|
|
345
|
+
button:active { opacity: 0.8; }
|
|
346
|
+
button:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
347
|
+
${variantRules}
|
|
348
|
+
${sizeRules}
|
|
349
|
+
`;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
function registerKlappayButton() {
|
|
353
|
+
if (!customElements.get(KLAPPAY_BUTTON_TAG)) {
|
|
354
|
+
customElements.define(KLAPPAY_BUTTON_TAG, KlappayButtonElement);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/react/index.tsx
|
|
359
|
+
import { jsx } from "react/jsx-runtime";
|
|
360
|
+
registerKlappayButton();
|
|
361
|
+
function useKlappayOne(config) {
|
|
362
|
+
const configRef = useRef(config);
|
|
363
|
+
configRef.current = config;
|
|
364
|
+
return {
|
|
365
|
+
open: () => createKlappayOne(configRef.current).open()
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function KlappayButton(props) {
|
|
369
|
+
const ref = useRef(null);
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
const el = ref.current;
|
|
372
|
+
if (!el) return;
|
|
373
|
+
function onSuccess(event) {
|
|
374
|
+
props.onSuccess?.(event.detail);
|
|
375
|
+
}
|
|
376
|
+
function onError(event) {
|
|
377
|
+
props.onError?.(event.detail);
|
|
378
|
+
}
|
|
379
|
+
function onCancel() {
|
|
380
|
+
props.onCancel?.();
|
|
381
|
+
}
|
|
382
|
+
el.addEventListener("success", onSuccess);
|
|
383
|
+
el.addEventListener("error", onError);
|
|
384
|
+
el.addEventListener("cancel", onCancel);
|
|
385
|
+
return () => {
|
|
386
|
+
el.removeEventListener("success", onSuccess);
|
|
387
|
+
el.removeEventListener("error", onError);
|
|
388
|
+
el.removeEventListener("cancel", onCancel);
|
|
389
|
+
};
|
|
390
|
+
}, [props.onSuccess, props.onError, props.onCancel]);
|
|
391
|
+
return /* @__PURE__ */ jsx(
|
|
392
|
+
"klappay-button",
|
|
393
|
+
{
|
|
394
|
+
ref,
|
|
395
|
+
"charge-id": props.chargeId,
|
|
396
|
+
origin: props.origin,
|
|
397
|
+
locale: props.locale,
|
|
398
|
+
variant: props.variant,
|
|
399
|
+
size: props.size
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
export {
|
|
404
|
+
KlappayButton,
|
|
405
|
+
useKlappayOne
|
|
406
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@klappay/one",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Klappay One's embeddable payment button — opens the hosted identity/wallet popup (one-id) and relays the result via postMessage. Never touches a private key or a session token; framework-agnostic core with a thin React wrapper.",
|
|
6
|
+
"keywords": ["klap", "klappay", "payments", "crypto", "web3", "widget"],
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/klappay/klap-one-js.git"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=24"
|
|
13
|
+
},
|
|
14
|
+
"packageManager": "pnpm@10.33.0",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": ["./dist/index.js", "./dist/index.global.js", "./dist/react/index.js"],
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./react": {
|
|
25
|
+
"types": "./dist/react/index.d.ts",
|
|
26
|
+
"import": "./dist/react/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": ["dist", "LICENSE", "README.md"],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "rm -rf dist && tsup",
|
|
35
|
+
"dev": "tsup --watch",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"lint": "biome check .",
|
|
38
|
+
"lint:fix": "biome check --write .",
|
|
39
|
+
"format": "biome format --write .",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"changeset": "changeset",
|
|
42
|
+
"version": "changeset version",
|
|
43
|
+
"release": "pnpm build && changeset publish",
|
|
44
|
+
"prepare": "husky"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"react": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@biomejs/biome": "^1.9.0",
|
|
56
|
+
"@changesets/cli": "^2.31.1",
|
|
57
|
+
"@commitlint/cli": "^19.5.0",
|
|
58
|
+
"@commitlint/config-conventional": "^19.5.0",
|
|
59
|
+
"@types/node": "^24.13.3",
|
|
60
|
+
"@types/react": "^18.3.0",
|
|
61
|
+
"husky": "^9.1.0",
|
|
62
|
+
"jsdom": "^30.0.1",
|
|
63
|
+
"lint-staged": "^15.2.0",
|
|
64
|
+
"react": "^18.3.0",
|
|
65
|
+
"tsup": "^8.3.0",
|
|
66
|
+
"typescript": "^5.6.0",
|
|
67
|
+
"vitest": "^3.2.6"
|
|
68
|
+
},
|
|
69
|
+
"pnpm": {
|
|
70
|
+
"onlyBuiltDependencies": ["@biomejs/biome", "esbuild"]
|
|
71
|
+
}
|
|
72
|
+
}
|