@canopypay/checkout-sdk 0.2.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/canopy.global.js +1 -0
- package/dist/index.cjs +347 -0
- package/dist/index.d.cts +120 -0
- package/dist/index.d.ts +120 -0
- package/dist/index.js +324 -0
- package/package.json +38 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";(()=>{(function(){"use strict";if(typeof window=="undefined")return;let f="width=460,height=640",m=2500,y="Pay with Canopy",h=500,g="canopy",v="cnpy_pk_live_";function b(){let e=document.currentScript;if(e&&e.src)try{return new URL(e.src).origin}catch(o){}let n=document.getElementsByTagName("script");for(let o=0;o<n.length;o+=1){let r=n[o].src;if(r&&/\/v1\/(?:[^/]+\/)?canopy\.js(?:[?#].*)?$/.test(r))try{return new URL(r).origin}catch(t){}}return null}function k(e){let n;try{n=new URL(e)}catch(t){return{ok:!1,code:"canopy_origin_invalid"}}if(!((n.pathname===""||n.pathname==="/")&&n.search===""&&n.hash===""))return{ok:!1,code:"canopy_origin_not_bare"};let r=n.hostname==="localhost"||n.hostname==="127.0.0.1";return n.protocol!=="https:"&&!r?{ok:!1,code:"canopy_origin_insecure"}:{ok:!0,origin:n.origin}}function _(e){if(e!==void 0)return k(e);let n=b();return n?{ok:!0,origin:n}:{ok:!1,code:"canopy_origin_undiscoverable"}}function c(e){let n=new URL("/customer-pay",e.origin);return n.searchParams.set("merchant",e.merchant),n.hash="cap="+encodeURIComponent(e.capability||""),n.toString()}function i(e,n,o){if(!e.destroyed)try{e.onEvent(Object.assign({type:n},o))}catch(r){}}function l(e){e.handshakeTimer&&(clearTimeout(e.handshakeTimer),e.handshakeTimer=null),e.resizeObserver&&(e.resizeObserver.disconnect(),e.resizeObserver=null),e.frame&&(e.frame.parentNode&&e.frame.parentNode.removeChild(e.frame),e.frame=null),e.lastHeight=null}function E(e){e.popupPollTimer&&clearInterval(e.popupPollTimer),e.popupPollTimer=setInterval(function(){try{e.popupWin&&e.popupWin.closed&&(e.popupPollTimer&&clearInterval(e.popupPollTimer),e.popupPollTimer=null,e.popupWin=null,i(e,"close",{via:"popup"}))}catch(n){e.popupPollTimer&&clearInterval(e.popupPollTimer),e.popupPollTimer=null}},h)}function p(e,n){if(!e.destroyed){if(!n){i(e,"error",{code:"popup_blocked"});return}if(!e.capability){try{n.close()}catch(o){}i(e,"error",{code:"capability_unavailable"});return}try{n.location.href=c(e)}catch(o){i(e,"error",{code:"popup_navigation_failed"});return}e.popupWin=n,E(e)}}function T(e){let n=document.createElement("button");n.type="button",n.textContent=e.fallbackLabel;let o=n.style;o.background="#1A7A4C",o.color="#FFFFFF",o.border="none",o.borderRadius="8px",o.height="48px",o.padding="0 20px",o.fontSize="16px",o.fontFamily="inherit",o.cursor="pointer",o.maxWidth="100%",o.overflow="hidden",o.textOverflow="ellipsis",o.whiteSpace="nowrap",o.display="inline-block",n.addEventListener("click",function(){let r=window.open("","canopy_checkout",f);p(e,r)}),e.node.appendChild(n),e.fallbackButton=n,i(e,"fallback_offered",{surface:"popup",reason:"iframe_unavailable"})}function w(e){let n=document.createElement("iframe");n.src=c(e),n.style.width="100%",n.style.border="none",e.node.appendChild(n),e.frame=n,typeof ResizeObserver!="undefined"&&(e.resizeObserver=new ResizeObserver(function(o){for(let r=0;r<o.length;r+=1){let t=Math.round(o[r].contentRect.height);t>0&&t!==e.lastHeight&&(e.lastHeight=t,i(e,"resize",{height:t}))}}),e.resizeObserver.observe(n)),e.handshakeTimer=setTimeout(function(){e.handshakeTimer=null,i(e,"surface_unavailable",{attempted:"iframe"}),l(e),e.surface==="auto"?T(e):i(e,"error",{code:"iframe_blocked"})},e.handshakeTimeoutMs)}function O(e,n){if(n.origin!==e.origin)return;let o=n.data;if(!(!o||o.source!==g)){if(o.type==="ready"){e.handshakeTimer&&(clearTimeout(e.handshakeTimer),e.handshakeTimer=null),i(e,"ready");return}if(o.type==="resize"&&typeof o.height=="number"&&e.frame){e.frame.style.height=o.height+"px";return}if(o.type==="paid"){i(e,"paid",o);return}if(o.type==="close"){i(e,"close");return}}}let s=new WeakMap;function L(e){if(typeof e.merchant!="string"||e.merchant.indexOf(v)!==0){try{(e.onEvent||function(){})({type:"error",code:"invalid_merchant_key"})}catch(a){}return{openNow:function(){},destroy:function(){}}}let n=_(e.canopyOrigin);if(!n.ok){try{(e.onEvent||function(){})({type:"error",code:n.code})}catch(a){}return{openNow:function(){},destroy:function(){}}}let o=typeof e.target=="string"?document.querySelector(e.target):e.target;if(!o)return{openNow:function(){},destroy:function(){}};let r=s.get(o);if(r)return r;let t={node:o,capability:e.capability,merchant:e.merchant,origin:n.origin,surface:e.surface||"popup",handshakeTimeoutMs:e.handshakeTimeoutMs||m,fallbackLabel:e.fallbackLabel||y,onEvent:e.onEvent||function(){},destroyed:!1,messageListener:function(){},handshakeTimer:null,resizeObserver:null,frame:null,fallbackButton:null,lastHeight:null,popupWin:null,popupPollTimer:null};t.messageListener=function(a){O(t,a)},window.addEventListener("message",t.messageListener),(t.surface==="iframe"||t.surface==="auto")&&w(t);let d={openNow:function(a){p(t,a)},destroy:function(){t.destroyed||(t.destroyed=!0,window.removeEventListener("message",t.messageListener),l(t),t.popupPollTimer&&(clearInterval(t.popupPollTimer),t.popupPollTimer=null),t.fallbackButton&&(t.fallbackButton.parentNode&&t.fallbackButton.parentNode.removeChild(t.fallbackButton),t.fallbackButton=null),s.delete(o))}};return s.set(o,d),d}let u=window;u.Canopy&&u.Canopy.__version==="2"||(u.Canopy={mount:L,__version:"2"})})();})();
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
mount: () => mount,
|
|
24
|
+
version: () => version
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(src_exports);
|
|
27
|
+
|
|
28
|
+
// src/loader.ts
|
|
29
|
+
var LOADER_VERSION = "2";
|
|
30
|
+
(function() {
|
|
31
|
+
"use strict";
|
|
32
|
+
if (typeof window === "undefined") return;
|
|
33
|
+
const POPUP_FEATURES = "width=460,height=640";
|
|
34
|
+
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 2500;
|
|
35
|
+
const DEFAULT_FALLBACK_LABEL = "Pay with Canopy";
|
|
36
|
+
const POPUP_POLL_INTERVAL_MS = 500;
|
|
37
|
+
const MESSAGE_SOURCE = "canopy";
|
|
38
|
+
const PUBLISHABLE_KEY_PREFIX = "cnpy_pk_live_";
|
|
39
|
+
function resolveOrigin() {
|
|
40
|
+
const current = document.currentScript;
|
|
41
|
+
if (current && current.src) {
|
|
42
|
+
try {
|
|
43
|
+
return new URL(current.src).origin;
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const scripts = document.getElementsByTagName("script");
|
|
48
|
+
for (let i = 0; i < scripts.length; i += 1) {
|
|
49
|
+
const src = scripts[i].src;
|
|
50
|
+
if (src && /\/v1\/(?:[^/]+\/)?canopy\.js(?:[?#].*)?$/.test(src)) {
|
|
51
|
+
try {
|
|
52
|
+
return new URL(src).origin;
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function validateExplicitOrigin(raw) {
|
|
60
|
+
let url;
|
|
61
|
+
try {
|
|
62
|
+
url = new URL(raw);
|
|
63
|
+
} catch {
|
|
64
|
+
return { ok: false, code: "canopy_origin_invalid" };
|
|
65
|
+
}
|
|
66
|
+
const isBare = (url.pathname === "" || url.pathname === "/") && url.search === "" && url.hash === "";
|
|
67
|
+
if (!isBare) {
|
|
68
|
+
return { ok: false, code: "canopy_origin_not_bare" };
|
|
69
|
+
}
|
|
70
|
+
const isDevHost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
71
|
+
if (url.protocol !== "https:" && !isDevHost) {
|
|
72
|
+
return { ok: false, code: "canopy_origin_insecure" };
|
|
73
|
+
}
|
|
74
|
+
return { ok: true, origin: url.origin };
|
|
75
|
+
}
|
|
76
|
+
function resolveMountOrigin(explicit) {
|
|
77
|
+
if (explicit !== void 0) {
|
|
78
|
+
return validateExplicitOrigin(explicit);
|
|
79
|
+
}
|
|
80
|
+
const scriptOrigin = resolveOrigin();
|
|
81
|
+
if (scriptOrigin) {
|
|
82
|
+
return { ok: true, origin: scriptOrigin };
|
|
83
|
+
}
|
|
84
|
+
return { ok: false, code: "canopy_origin_undiscoverable" };
|
|
85
|
+
}
|
|
86
|
+
function buildCheckoutUrl(internal) {
|
|
87
|
+
const url = new URL("/customer-pay", internal.origin);
|
|
88
|
+
url.searchParams.set("merchant", internal.merchant);
|
|
89
|
+
url.hash = "cap=" + encodeURIComponent(internal.capability || "");
|
|
90
|
+
return url.toString();
|
|
91
|
+
}
|
|
92
|
+
function emit(internal, type, data) {
|
|
93
|
+
if (internal.destroyed) return;
|
|
94
|
+
try {
|
|
95
|
+
internal.onEvent(Object.assign({ type }, data));
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function teardownFrame(internal) {
|
|
100
|
+
if (internal.handshakeTimer) {
|
|
101
|
+
clearTimeout(internal.handshakeTimer);
|
|
102
|
+
internal.handshakeTimer = null;
|
|
103
|
+
}
|
|
104
|
+
if (internal.resizeObserver) {
|
|
105
|
+
internal.resizeObserver.disconnect();
|
|
106
|
+
internal.resizeObserver = null;
|
|
107
|
+
}
|
|
108
|
+
if (internal.frame) {
|
|
109
|
+
if (internal.frame.parentNode)
|
|
110
|
+
internal.frame.parentNode.removeChild(internal.frame);
|
|
111
|
+
internal.frame = null;
|
|
112
|
+
}
|
|
113
|
+
internal.lastHeight = null;
|
|
114
|
+
}
|
|
115
|
+
function startPopupClosePolling(internal) {
|
|
116
|
+
if (internal.popupPollTimer) clearInterval(internal.popupPollTimer);
|
|
117
|
+
internal.popupPollTimer = setInterval(function() {
|
|
118
|
+
try {
|
|
119
|
+
if (internal.popupWin && internal.popupWin.closed) {
|
|
120
|
+
if (internal.popupPollTimer) clearInterval(internal.popupPollTimer);
|
|
121
|
+
internal.popupPollTimer = null;
|
|
122
|
+
internal.popupWin = null;
|
|
123
|
+
emit(internal, "close", { via: "popup" });
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
if (internal.popupPollTimer) clearInterval(internal.popupPollTimer);
|
|
127
|
+
internal.popupPollTimer = null;
|
|
128
|
+
}
|
|
129
|
+
}, POPUP_POLL_INTERVAL_MS);
|
|
130
|
+
}
|
|
131
|
+
function openNowInternal(internal, win) {
|
|
132
|
+
if (internal.destroyed) return;
|
|
133
|
+
if (!win) {
|
|
134
|
+
emit(internal, "error", { code: "popup_blocked" });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!internal.capability) {
|
|
138
|
+
try {
|
|
139
|
+
win.close();
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
emit(internal, "error", { code: "capability_unavailable" });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
win.location.href = buildCheckoutUrl(internal);
|
|
147
|
+
} catch {
|
|
148
|
+
emit(internal, "error", { code: "popup_navigation_failed" });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
internal.popupWin = win;
|
|
152
|
+
startPopupClosePolling(internal);
|
|
153
|
+
}
|
|
154
|
+
function offerFallback(internal) {
|
|
155
|
+
const button = document.createElement("button");
|
|
156
|
+
button.type = "button";
|
|
157
|
+
button.textContent = internal.fallbackLabel;
|
|
158
|
+
const style = button.style;
|
|
159
|
+
style.background = "#1A7A4C";
|
|
160
|
+
style.color = "#FFFFFF";
|
|
161
|
+
style.border = "none";
|
|
162
|
+
style.borderRadius = "8px";
|
|
163
|
+
style.height = "48px";
|
|
164
|
+
style.padding = "0 20px";
|
|
165
|
+
style.fontSize = "16px";
|
|
166
|
+
style.fontFamily = "inherit";
|
|
167
|
+
style.cursor = "pointer";
|
|
168
|
+
style.maxWidth = "100%";
|
|
169
|
+
style.overflow = "hidden";
|
|
170
|
+
style.textOverflow = "ellipsis";
|
|
171
|
+
style.whiteSpace = "nowrap";
|
|
172
|
+
style.display = "inline-block";
|
|
173
|
+
button.addEventListener("click", function() {
|
|
174
|
+
const win = window.open("", "canopy_checkout", POPUP_FEATURES);
|
|
175
|
+
openNowInternal(internal, win);
|
|
176
|
+
});
|
|
177
|
+
internal.node.appendChild(button);
|
|
178
|
+
internal.fallbackButton = button;
|
|
179
|
+
emit(internal, "fallback_offered", {
|
|
180
|
+
surface: "popup",
|
|
181
|
+
reason: "iframe_unavailable"
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function createFrame(internal) {
|
|
185
|
+
const iframe = document.createElement("iframe");
|
|
186
|
+
iframe.src = buildCheckoutUrl(internal);
|
|
187
|
+
iframe.style.width = "100%";
|
|
188
|
+
iframe.style.border = "none";
|
|
189
|
+
internal.node.appendChild(iframe);
|
|
190
|
+
internal.frame = iframe;
|
|
191
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
192
|
+
internal.resizeObserver = new ResizeObserver(function(entries) {
|
|
193
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
194
|
+
const height = Math.round(entries[i].contentRect.height);
|
|
195
|
+
if (height > 0 && height !== internal.lastHeight) {
|
|
196
|
+
internal.lastHeight = height;
|
|
197
|
+
emit(internal, "resize", { height });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
internal.resizeObserver.observe(iframe);
|
|
202
|
+
}
|
|
203
|
+
internal.handshakeTimer = setTimeout(function() {
|
|
204
|
+
internal.handshakeTimer = null;
|
|
205
|
+
emit(internal, "surface_unavailable", { attempted: "iframe" });
|
|
206
|
+
teardownFrame(internal);
|
|
207
|
+
if (internal.surface === "auto") {
|
|
208
|
+
offerFallback(internal);
|
|
209
|
+
} else {
|
|
210
|
+
emit(internal, "error", { code: "iframe_blocked" });
|
|
211
|
+
}
|
|
212
|
+
}, internal.handshakeTimeoutMs);
|
|
213
|
+
}
|
|
214
|
+
function handleMessage(internal, event) {
|
|
215
|
+
if (event.origin !== internal.origin) return;
|
|
216
|
+
const data = event.data;
|
|
217
|
+
if (!data || data.source !== MESSAGE_SOURCE) return;
|
|
218
|
+
if (data.type === "ready") {
|
|
219
|
+
if (internal.handshakeTimer) {
|
|
220
|
+
clearTimeout(internal.handshakeTimer);
|
|
221
|
+
internal.handshakeTimer = null;
|
|
222
|
+
}
|
|
223
|
+
emit(internal, "ready");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (data.type === "resize" && typeof data.height === "number" && internal.frame) {
|
|
227
|
+
internal.frame.style.height = data.height + "px";
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (data.type === "paid") {
|
|
231
|
+
emit(internal, "paid", data);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (data.type === "close") {
|
|
235
|
+
emit(internal, "close");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const mounts = /* @__PURE__ */ new WeakMap();
|
|
240
|
+
function mount2(opts) {
|
|
241
|
+
if (typeof opts.merchant !== "string" || opts.merchant.indexOf(PUBLISHABLE_KEY_PREFIX) !== 0) {
|
|
242
|
+
try {
|
|
243
|
+
(opts.onEvent || function() {
|
|
244
|
+
})({
|
|
245
|
+
type: "error",
|
|
246
|
+
code: "invalid_merchant_key"
|
|
247
|
+
});
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
return { openNow: function() {
|
|
251
|
+
}, destroy: function() {
|
|
252
|
+
} };
|
|
253
|
+
}
|
|
254
|
+
const originResolution = resolveMountOrigin(opts.canopyOrigin);
|
|
255
|
+
if (!originResolution.ok) {
|
|
256
|
+
try {
|
|
257
|
+
(opts.onEvent || function() {
|
|
258
|
+
})({
|
|
259
|
+
type: "error",
|
|
260
|
+
code: originResolution.code
|
|
261
|
+
});
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
return { openNow: function() {
|
|
265
|
+
}, destroy: function() {
|
|
266
|
+
} };
|
|
267
|
+
}
|
|
268
|
+
const node = typeof opts.target === "string" ? document.querySelector(opts.target) : opts.target;
|
|
269
|
+
if (!node) {
|
|
270
|
+
return { openNow: function() {
|
|
271
|
+
}, destroy: function() {
|
|
272
|
+
} };
|
|
273
|
+
}
|
|
274
|
+
const existing = mounts.get(node);
|
|
275
|
+
if (existing) return existing;
|
|
276
|
+
const internal = {
|
|
277
|
+
node,
|
|
278
|
+
capability: opts.capability,
|
|
279
|
+
merchant: opts.merchant,
|
|
280
|
+
origin: originResolution.origin,
|
|
281
|
+
surface: opts.surface || "popup",
|
|
282
|
+
handshakeTimeoutMs: opts.handshakeTimeoutMs || DEFAULT_HANDSHAKE_TIMEOUT_MS,
|
|
283
|
+
fallbackLabel: opts.fallbackLabel || DEFAULT_FALLBACK_LABEL,
|
|
284
|
+
onEvent: opts.onEvent || function() {
|
|
285
|
+
},
|
|
286
|
+
destroyed: false,
|
|
287
|
+
messageListener: function() {
|
|
288
|
+
},
|
|
289
|
+
handshakeTimer: null,
|
|
290
|
+
resizeObserver: null,
|
|
291
|
+
frame: null,
|
|
292
|
+
fallbackButton: null,
|
|
293
|
+
lastHeight: null,
|
|
294
|
+
popupWin: null,
|
|
295
|
+
popupPollTimer: null
|
|
296
|
+
};
|
|
297
|
+
internal.messageListener = function(event) {
|
|
298
|
+
handleMessage(internal, event);
|
|
299
|
+
};
|
|
300
|
+
window.addEventListener("message", internal.messageListener);
|
|
301
|
+
if (internal.surface === "iframe" || internal.surface === "auto") {
|
|
302
|
+
createFrame(internal);
|
|
303
|
+
}
|
|
304
|
+
const handle = {
|
|
305
|
+
openNow: function(win) {
|
|
306
|
+
openNowInternal(internal, win);
|
|
307
|
+
},
|
|
308
|
+
destroy: function() {
|
|
309
|
+
if (internal.destroyed) return;
|
|
310
|
+
internal.destroyed = true;
|
|
311
|
+
window.removeEventListener("message", internal.messageListener);
|
|
312
|
+
teardownFrame(internal);
|
|
313
|
+
if (internal.popupPollTimer) {
|
|
314
|
+
clearInterval(internal.popupPollTimer);
|
|
315
|
+
internal.popupPollTimer = null;
|
|
316
|
+
}
|
|
317
|
+
if (internal.fallbackButton) {
|
|
318
|
+
if (internal.fallbackButton.parentNode) {
|
|
319
|
+
internal.fallbackButton.parentNode.removeChild(
|
|
320
|
+
internal.fallbackButton
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
internal.fallbackButton = null;
|
|
324
|
+
}
|
|
325
|
+
mounts.delete(node);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
mounts.set(node, handle);
|
|
329
|
+
return handle;
|
|
330
|
+
}
|
|
331
|
+
const globalWindow = window;
|
|
332
|
+
if (globalWindow.Canopy && globalWindow.Canopy.__version === LOADER_VERSION) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
globalWindow.Canopy = { mount: mount2, __version: LOADER_VERSION };
|
|
336
|
+
})();
|
|
337
|
+
|
|
338
|
+
// src/index.ts
|
|
339
|
+
function mount(opts) {
|
|
340
|
+
if (typeof window === "undefined" || !window.Canopy) {
|
|
341
|
+
throw new Error("@canopypay/checkout-sdk: loader did not initialise");
|
|
342
|
+
}
|
|
343
|
+
return window.Canopy.mount(opts);
|
|
344
|
+
}
|
|
345
|
+
function version() {
|
|
346
|
+
return typeof window === "undefined" ? void 0 : window.Canopy?.__version;
|
|
347
|
+
}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canopy checkout SDK loader (Phase 23 Plan 08, SDK-01 / SDK-04 / API-03).
|
|
3
|
+
*
|
|
4
|
+
* The whole merchant integration, measured (`sdk-loader-contract.md`):
|
|
5
|
+
*
|
|
6
|
+
* ```html
|
|
7
|
+
* <script src="https://canopy.example/v1/canopy.js"></script>
|
|
8
|
+
* <script type="module">
|
|
9
|
+
* const { capability } = await (await fetch("/my-server/create-canopy-intent")).json();
|
|
10
|
+
*
|
|
11
|
+
* const handle = Canopy.mount({
|
|
12
|
+
* target: "#checkout",
|
|
13
|
+
* capability,
|
|
14
|
+
* merchant: "cnpy_pk_live_abc123",
|
|
15
|
+
* onEvent: (e) => {
|
|
16
|
+
* // SDK-04: `paid` is advisory only -- it drives UI, never fulfilment.
|
|
17
|
+
* // Fulfilment reads a webhook or a server-side status call.
|
|
18
|
+
* if (e.type === "paid") showBanner("Payment sent — we'll confirm shortly");
|
|
19
|
+
* },
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* payButton.addEventListener("click", () => {
|
|
23
|
+
* // The gesture cannot be manufactured -- open synchronously inside the
|
|
24
|
+
* // merchant's own click handler, then hand the blank window to the SDK.
|
|
25
|
+
* handle.openNow(window.open("", "canopy_checkout", "width=460,height=640"));
|
|
26
|
+
* });
|
|
27
|
+
* </script>
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* One entry point (`mount`), one option surface (`surface: "popup" | "iframe"
|
|
31
|
+
* | "auto"`, default `"popup"`), seven events (`ready`, `resize`, `paid`,
|
|
32
|
+
* `error`, `close`, `surface_unavailable`, `fallback_offered`). See
|
|
33
|
+
* `.claude/skills/spike-findings-Canopy/references/sdk-loader-contract.md`
|
|
34
|
+
* and `.claude/skills/spike-findings-Canopy/references/embed-surface-selection.md`
|
|
35
|
+
* for the full contract this file implements.
|
|
36
|
+
*
|
|
37
|
+
* Bump LOADER_VERSION whenever this file's runtime behaviour changes.
|
|
38
|
+
* `scripts/build-checkout-sdk.ts` extracts this exact literal by regex from
|
|
39
|
+
* this source file (not the built bundle) so the immutable serving path
|
|
40
|
+
* (`/v1/{version}/canopy.js`) and this loader's own `Canopy.__version`
|
|
41
|
+
* self-report are always the same value by construction.
|
|
42
|
+
*
|
|
43
|
+
* Phase 31 Plan 02 (DIST-01, DIST-04): the five types below are hoisted
|
|
44
|
+
* above the IIFE and `export type`-ed so `packages/checkout-sdk/src/index.ts`
|
|
45
|
+
* (the npm entry) can re-export a real merchant-facing surface instead of
|
|
46
|
+
* shipping a 13-byte `export { }` declaration file. This is byte-safe --
|
|
47
|
+
* type-only exports are erased before esbuild picks its format wrapper, so
|
|
48
|
+
* the IIFE build this script tag serves is unaffected (verified sha
|
|
49
|
+
* `644fbe336e875f4f` both ways, spike 100 exp2/exp3). `Internal` stays
|
|
50
|
+
* inside the IIFE: it is not part of the merchant-facing surface.
|
|
51
|
+
*/
|
|
52
|
+
type CanopyEventType = "ready" | "resize" | "paid" | "error" | "close" | "surface_unavailable" | "fallback_offered";
|
|
53
|
+
type CanopyEvent = {
|
|
54
|
+
type: CanopyEventType;
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
};
|
|
57
|
+
type Surface = "popup" | "iframe" | "auto";
|
|
58
|
+
type MountOptions = {
|
|
59
|
+
target: string | Element;
|
|
60
|
+
capability?: string;
|
|
61
|
+
merchant: string;
|
|
62
|
+
surface?: Surface;
|
|
63
|
+
onEvent?: (event: CanopyEvent) => void;
|
|
64
|
+
handshakeTimeoutMs?: number;
|
|
65
|
+
fallbackLabel?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Phase 31 Plan 05 (DIST-02): the Canopy origin `mount()` builds the
|
|
68
|
+
* checkout URL against and pins the `postMessage` listener to. A
|
|
69
|
+
* `<script src="https://.../v1/canopy.js">` tag can be discovered
|
|
70
|
+
* automatically; a bundled npm consumer has no such tag, so it MUST state
|
|
71
|
+
* this explicitly or `mount()` refuses with `canopy_origin_undiscoverable`.
|
|
72
|
+
* There is no default -- baking one in would turn that loud, diagnosable
|
|
73
|
+
* refusal into a silent wrong-origin checkout for every sandbox or
|
|
74
|
+
* self-hosted integration (31-RESEARCH.md Pitfall 5). Must be a bare
|
|
75
|
+
* origin (no path, search, or hash) and `https`, except for the
|
|
76
|
+
* `localhost` / `127.0.0.1` development affordance. Wins over a present
|
|
77
|
+
* script tag when both are available.
|
|
78
|
+
*/
|
|
79
|
+
canopyOrigin?: string;
|
|
80
|
+
};
|
|
81
|
+
type CanopyHandle = {
|
|
82
|
+
openNow: (win: Window | null) => void;
|
|
83
|
+
destroy: () => void;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Canopy checkout SDK npm entry (Phase 31 Plan 02, DIST-01 / DIST-04).
|
|
88
|
+
*
|
|
89
|
+
* This is the `npm i @canopypay/checkout-sdk` consumer surface -- it delegates
|
|
90
|
+
* to `./loader`'s IIFE (imported here for its side effect of installing
|
|
91
|
+
* `window.Canopy`) rather than re-implementing `mount()`. A second
|
|
92
|
+
* implementation of the security-relevant origin pinning and the
|
|
93
|
+
* publishable-key prefix check would fork exactly the mechanism DIST-01
|
|
94
|
+
* exists to keep singular. See
|
|
95
|
+
* `.claude/skills/spike-findings-Canopy/references/sdk-package-distribution.md`
|
|
96
|
+
* § "How to Build It" step 2 for the verified shape this file follows.
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
declare global {
|
|
100
|
+
interface Window {
|
|
101
|
+
Canopy?: {
|
|
102
|
+
mount: (o: MountOptions) => CanopyHandle;
|
|
103
|
+
__version: string;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Delegates directly to `window.Canopy.mount` -- the loader's side-effect
|
|
109
|
+
* import above must have already run and installed the global by the time
|
|
110
|
+
* a bundler-consuming merchant calls this. Throws (rather than degrading to
|
|
111
|
+
* a no-op handle, which is the script-tag loader's own integration-error
|
|
112
|
+
* fallback) because a bundled consumer with no `window.Canopy` indicates the
|
|
113
|
+
* package itself failed to initialise, not a merchant mistake.
|
|
114
|
+
*/
|
|
115
|
+
declare function mount(opts: MountOptions): CanopyHandle;
|
|
116
|
+
/** Reports the running loader's self-declared `LOADER_VERSION`, or
|
|
117
|
+
* `undefined` server-side / before the loader has installed the global. */
|
|
118
|
+
declare function version(): string | undefined;
|
|
119
|
+
|
|
120
|
+
export { type CanopyEvent, type CanopyEventType, type CanopyHandle, type MountOptions, type Surface, mount, version };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canopy checkout SDK loader (Phase 23 Plan 08, SDK-01 / SDK-04 / API-03).
|
|
3
|
+
*
|
|
4
|
+
* The whole merchant integration, measured (`sdk-loader-contract.md`):
|
|
5
|
+
*
|
|
6
|
+
* ```html
|
|
7
|
+
* <script src="https://canopy.example/v1/canopy.js"></script>
|
|
8
|
+
* <script type="module">
|
|
9
|
+
* const { capability } = await (await fetch("/my-server/create-canopy-intent")).json();
|
|
10
|
+
*
|
|
11
|
+
* const handle = Canopy.mount({
|
|
12
|
+
* target: "#checkout",
|
|
13
|
+
* capability,
|
|
14
|
+
* merchant: "cnpy_pk_live_abc123",
|
|
15
|
+
* onEvent: (e) => {
|
|
16
|
+
* // SDK-04: `paid` is advisory only -- it drives UI, never fulfilment.
|
|
17
|
+
* // Fulfilment reads a webhook or a server-side status call.
|
|
18
|
+
* if (e.type === "paid") showBanner("Payment sent — we'll confirm shortly");
|
|
19
|
+
* },
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* payButton.addEventListener("click", () => {
|
|
23
|
+
* // The gesture cannot be manufactured -- open synchronously inside the
|
|
24
|
+
* // merchant's own click handler, then hand the blank window to the SDK.
|
|
25
|
+
* handle.openNow(window.open("", "canopy_checkout", "width=460,height=640"));
|
|
26
|
+
* });
|
|
27
|
+
* </script>
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* One entry point (`mount`), one option surface (`surface: "popup" | "iframe"
|
|
31
|
+
* | "auto"`, default `"popup"`), seven events (`ready`, `resize`, `paid`,
|
|
32
|
+
* `error`, `close`, `surface_unavailable`, `fallback_offered`). See
|
|
33
|
+
* `.claude/skills/spike-findings-Canopy/references/sdk-loader-contract.md`
|
|
34
|
+
* and `.claude/skills/spike-findings-Canopy/references/embed-surface-selection.md`
|
|
35
|
+
* for the full contract this file implements.
|
|
36
|
+
*
|
|
37
|
+
* Bump LOADER_VERSION whenever this file's runtime behaviour changes.
|
|
38
|
+
* `scripts/build-checkout-sdk.ts` extracts this exact literal by regex from
|
|
39
|
+
* this source file (not the built bundle) so the immutable serving path
|
|
40
|
+
* (`/v1/{version}/canopy.js`) and this loader's own `Canopy.__version`
|
|
41
|
+
* self-report are always the same value by construction.
|
|
42
|
+
*
|
|
43
|
+
* Phase 31 Plan 02 (DIST-01, DIST-04): the five types below are hoisted
|
|
44
|
+
* above the IIFE and `export type`-ed so `packages/checkout-sdk/src/index.ts`
|
|
45
|
+
* (the npm entry) can re-export a real merchant-facing surface instead of
|
|
46
|
+
* shipping a 13-byte `export { }` declaration file. This is byte-safe --
|
|
47
|
+
* type-only exports are erased before esbuild picks its format wrapper, so
|
|
48
|
+
* the IIFE build this script tag serves is unaffected (verified sha
|
|
49
|
+
* `644fbe336e875f4f` both ways, spike 100 exp2/exp3). `Internal` stays
|
|
50
|
+
* inside the IIFE: it is not part of the merchant-facing surface.
|
|
51
|
+
*/
|
|
52
|
+
type CanopyEventType = "ready" | "resize" | "paid" | "error" | "close" | "surface_unavailable" | "fallback_offered";
|
|
53
|
+
type CanopyEvent = {
|
|
54
|
+
type: CanopyEventType;
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
};
|
|
57
|
+
type Surface = "popup" | "iframe" | "auto";
|
|
58
|
+
type MountOptions = {
|
|
59
|
+
target: string | Element;
|
|
60
|
+
capability?: string;
|
|
61
|
+
merchant: string;
|
|
62
|
+
surface?: Surface;
|
|
63
|
+
onEvent?: (event: CanopyEvent) => void;
|
|
64
|
+
handshakeTimeoutMs?: number;
|
|
65
|
+
fallbackLabel?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Phase 31 Plan 05 (DIST-02): the Canopy origin `mount()` builds the
|
|
68
|
+
* checkout URL against and pins the `postMessage` listener to. A
|
|
69
|
+
* `<script src="https://.../v1/canopy.js">` tag can be discovered
|
|
70
|
+
* automatically; a bundled npm consumer has no such tag, so it MUST state
|
|
71
|
+
* this explicitly or `mount()` refuses with `canopy_origin_undiscoverable`.
|
|
72
|
+
* There is no default -- baking one in would turn that loud, diagnosable
|
|
73
|
+
* refusal into a silent wrong-origin checkout for every sandbox or
|
|
74
|
+
* self-hosted integration (31-RESEARCH.md Pitfall 5). Must be a bare
|
|
75
|
+
* origin (no path, search, or hash) and `https`, except for the
|
|
76
|
+
* `localhost` / `127.0.0.1` development affordance. Wins over a present
|
|
77
|
+
* script tag when both are available.
|
|
78
|
+
*/
|
|
79
|
+
canopyOrigin?: string;
|
|
80
|
+
};
|
|
81
|
+
type CanopyHandle = {
|
|
82
|
+
openNow: (win: Window | null) => void;
|
|
83
|
+
destroy: () => void;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Canopy checkout SDK npm entry (Phase 31 Plan 02, DIST-01 / DIST-04).
|
|
88
|
+
*
|
|
89
|
+
* This is the `npm i @canopypay/checkout-sdk` consumer surface -- it delegates
|
|
90
|
+
* to `./loader`'s IIFE (imported here for its side effect of installing
|
|
91
|
+
* `window.Canopy`) rather than re-implementing `mount()`. A second
|
|
92
|
+
* implementation of the security-relevant origin pinning and the
|
|
93
|
+
* publishable-key prefix check would fork exactly the mechanism DIST-01
|
|
94
|
+
* exists to keep singular. See
|
|
95
|
+
* `.claude/skills/spike-findings-Canopy/references/sdk-package-distribution.md`
|
|
96
|
+
* § "How to Build It" step 2 for the verified shape this file follows.
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
declare global {
|
|
100
|
+
interface Window {
|
|
101
|
+
Canopy?: {
|
|
102
|
+
mount: (o: MountOptions) => CanopyHandle;
|
|
103
|
+
__version: string;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Delegates directly to `window.Canopy.mount` -- the loader's side-effect
|
|
109
|
+
* import above must have already run and installed the global by the time
|
|
110
|
+
* a bundler-consuming merchant calls this. Throws (rather than degrading to
|
|
111
|
+
* a no-op handle, which is the script-tag loader's own integration-error
|
|
112
|
+
* fallback) because a bundled consumer with no `window.Canopy` indicates the
|
|
113
|
+
* package itself failed to initialise, not a merchant mistake.
|
|
114
|
+
*/
|
|
115
|
+
declare function mount(opts: MountOptions): CanopyHandle;
|
|
116
|
+
/** Reports the running loader's self-declared `LOADER_VERSION`, or
|
|
117
|
+
* `undefined` server-side / before the loader has installed the global. */
|
|
118
|
+
declare function version(): string | undefined;
|
|
119
|
+
|
|
120
|
+
export { type CanopyEvent, type CanopyEventType, type CanopyHandle, type MountOptions, type Surface, mount, version };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// src/loader.ts
|
|
2
|
+
var LOADER_VERSION = "2";
|
|
3
|
+
(function() {
|
|
4
|
+
"use strict";
|
|
5
|
+
if (typeof window === "undefined") return;
|
|
6
|
+
const POPUP_FEATURES = "width=460,height=640";
|
|
7
|
+
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 2500;
|
|
8
|
+
const DEFAULT_FALLBACK_LABEL = "Pay with Canopy";
|
|
9
|
+
const POPUP_POLL_INTERVAL_MS = 500;
|
|
10
|
+
const MESSAGE_SOURCE = "canopy";
|
|
11
|
+
const PUBLISHABLE_KEY_PREFIX = "cnpy_pk_live_";
|
|
12
|
+
function resolveOrigin() {
|
|
13
|
+
const current = document.currentScript;
|
|
14
|
+
if (current && current.src) {
|
|
15
|
+
try {
|
|
16
|
+
return new URL(current.src).origin;
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const scripts = document.getElementsByTagName("script");
|
|
21
|
+
for (let i = 0; i < scripts.length; i += 1) {
|
|
22
|
+
const src = scripts[i].src;
|
|
23
|
+
if (src && /\/v1\/(?:[^/]+\/)?canopy\.js(?:[?#].*)?$/.test(src)) {
|
|
24
|
+
try {
|
|
25
|
+
return new URL(src).origin;
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function validateExplicitOrigin(raw) {
|
|
33
|
+
let url;
|
|
34
|
+
try {
|
|
35
|
+
url = new URL(raw);
|
|
36
|
+
} catch {
|
|
37
|
+
return { ok: false, code: "canopy_origin_invalid" };
|
|
38
|
+
}
|
|
39
|
+
const isBare = (url.pathname === "" || url.pathname === "/") && url.search === "" && url.hash === "";
|
|
40
|
+
if (!isBare) {
|
|
41
|
+
return { ok: false, code: "canopy_origin_not_bare" };
|
|
42
|
+
}
|
|
43
|
+
const isDevHost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
44
|
+
if (url.protocol !== "https:" && !isDevHost) {
|
|
45
|
+
return { ok: false, code: "canopy_origin_insecure" };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, origin: url.origin };
|
|
48
|
+
}
|
|
49
|
+
function resolveMountOrigin(explicit) {
|
|
50
|
+
if (explicit !== void 0) {
|
|
51
|
+
return validateExplicitOrigin(explicit);
|
|
52
|
+
}
|
|
53
|
+
const scriptOrigin = resolveOrigin();
|
|
54
|
+
if (scriptOrigin) {
|
|
55
|
+
return { ok: true, origin: scriptOrigin };
|
|
56
|
+
}
|
|
57
|
+
return { ok: false, code: "canopy_origin_undiscoverable" };
|
|
58
|
+
}
|
|
59
|
+
function buildCheckoutUrl(internal) {
|
|
60
|
+
const url = new URL("/customer-pay", internal.origin);
|
|
61
|
+
url.searchParams.set("merchant", internal.merchant);
|
|
62
|
+
url.hash = "cap=" + encodeURIComponent(internal.capability || "");
|
|
63
|
+
return url.toString();
|
|
64
|
+
}
|
|
65
|
+
function emit(internal, type, data) {
|
|
66
|
+
if (internal.destroyed) return;
|
|
67
|
+
try {
|
|
68
|
+
internal.onEvent(Object.assign({ type }, data));
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function teardownFrame(internal) {
|
|
73
|
+
if (internal.handshakeTimer) {
|
|
74
|
+
clearTimeout(internal.handshakeTimer);
|
|
75
|
+
internal.handshakeTimer = null;
|
|
76
|
+
}
|
|
77
|
+
if (internal.resizeObserver) {
|
|
78
|
+
internal.resizeObserver.disconnect();
|
|
79
|
+
internal.resizeObserver = null;
|
|
80
|
+
}
|
|
81
|
+
if (internal.frame) {
|
|
82
|
+
if (internal.frame.parentNode)
|
|
83
|
+
internal.frame.parentNode.removeChild(internal.frame);
|
|
84
|
+
internal.frame = null;
|
|
85
|
+
}
|
|
86
|
+
internal.lastHeight = null;
|
|
87
|
+
}
|
|
88
|
+
function startPopupClosePolling(internal) {
|
|
89
|
+
if (internal.popupPollTimer) clearInterval(internal.popupPollTimer);
|
|
90
|
+
internal.popupPollTimer = setInterval(function() {
|
|
91
|
+
try {
|
|
92
|
+
if (internal.popupWin && internal.popupWin.closed) {
|
|
93
|
+
if (internal.popupPollTimer) clearInterval(internal.popupPollTimer);
|
|
94
|
+
internal.popupPollTimer = null;
|
|
95
|
+
internal.popupWin = null;
|
|
96
|
+
emit(internal, "close", { via: "popup" });
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
if (internal.popupPollTimer) clearInterval(internal.popupPollTimer);
|
|
100
|
+
internal.popupPollTimer = null;
|
|
101
|
+
}
|
|
102
|
+
}, POPUP_POLL_INTERVAL_MS);
|
|
103
|
+
}
|
|
104
|
+
function openNowInternal(internal, win) {
|
|
105
|
+
if (internal.destroyed) return;
|
|
106
|
+
if (!win) {
|
|
107
|
+
emit(internal, "error", { code: "popup_blocked" });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!internal.capability) {
|
|
111
|
+
try {
|
|
112
|
+
win.close();
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
emit(internal, "error", { code: "capability_unavailable" });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
win.location.href = buildCheckoutUrl(internal);
|
|
120
|
+
} catch {
|
|
121
|
+
emit(internal, "error", { code: "popup_navigation_failed" });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
internal.popupWin = win;
|
|
125
|
+
startPopupClosePolling(internal);
|
|
126
|
+
}
|
|
127
|
+
function offerFallback(internal) {
|
|
128
|
+
const button = document.createElement("button");
|
|
129
|
+
button.type = "button";
|
|
130
|
+
button.textContent = internal.fallbackLabel;
|
|
131
|
+
const style = button.style;
|
|
132
|
+
style.background = "#1A7A4C";
|
|
133
|
+
style.color = "#FFFFFF";
|
|
134
|
+
style.border = "none";
|
|
135
|
+
style.borderRadius = "8px";
|
|
136
|
+
style.height = "48px";
|
|
137
|
+
style.padding = "0 20px";
|
|
138
|
+
style.fontSize = "16px";
|
|
139
|
+
style.fontFamily = "inherit";
|
|
140
|
+
style.cursor = "pointer";
|
|
141
|
+
style.maxWidth = "100%";
|
|
142
|
+
style.overflow = "hidden";
|
|
143
|
+
style.textOverflow = "ellipsis";
|
|
144
|
+
style.whiteSpace = "nowrap";
|
|
145
|
+
style.display = "inline-block";
|
|
146
|
+
button.addEventListener("click", function() {
|
|
147
|
+
const win = window.open("", "canopy_checkout", POPUP_FEATURES);
|
|
148
|
+
openNowInternal(internal, win);
|
|
149
|
+
});
|
|
150
|
+
internal.node.appendChild(button);
|
|
151
|
+
internal.fallbackButton = button;
|
|
152
|
+
emit(internal, "fallback_offered", {
|
|
153
|
+
surface: "popup",
|
|
154
|
+
reason: "iframe_unavailable"
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function createFrame(internal) {
|
|
158
|
+
const iframe = document.createElement("iframe");
|
|
159
|
+
iframe.src = buildCheckoutUrl(internal);
|
|
160
|
+
iframe.style.width = "100%";
|
|
161
|
+
iframe.style.border = "none";
|
|
162
|
+
internal.node.appendChild(iframe);
|
|
163
|
+
internal.frame = iframe;
|
|
164
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
165
|
+
internal.resizeObserver = new ResizeObserver(function(entries) {
|
|
166
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
167
|
+
const height = Math.round(entries[i].contentRect.height);
|
|
168
|
+
if (height > 0 && height !== internal.lastHeight) {
|
|
169
|
+
internal.lastHeight = height;
|
|
170
|
+
emit(internal, "resize", { height });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
internal.resizeObserver.observe(iframe);
|
|
175
|
+
}
|
|
176
|
+
internal.handshakeTimer = setTimeout(function() {
|
|
177
|
+
internal.handshakeTimer = null;
|
|
178
|
+
emit(internal, "surface_unavailable", { attempted: "iframe" });
|
|
179
|
+
teardownFrame(internal);
|
|
180
|
+
if (internal.surface === "auto") {
|
|
181
|
+
offerFallback(internal);
|
|
182
|
+
} else {
|
|
183
|
+
emit(internal, "error", { code: "iframe_blocked" });
|
|
184
|
+
}
|
|
185
|
+
}, internal.handshakeTimeoutMs);
|
|
186
|
+
}
|
|
187
|
+
function handleMessage(internal, event) {
|
|
188
|
+
if (event.origin !== internal.origin) return;
|
|
189
|
+
const data = event.data;
|
|
190
|
+
if (!data || data.source !== MESSAGE_SOURCE) return;
|
|
191
|
+
if (data.type === "ready") {
|
|
192
|
+
if (internal.handshakeTimer) {
|
|
193
|
+
clearTimeout(internal.handshakeTimer);
|
|
194
|
+
internal.handshakeTimer = null;
|
|
195
|
+
}
|
|
196
|
+
emit(internal, "ready");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (data.type === "resize" && typeof data.height === "number" && internal.frame) {
|
|
200
|
+
internal.frame.style.height = data.height + "px";
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (data.type === "paid") {
|
|
204
|
+
emit(internal, "paid", data);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (data.type === "close") {
|
|
208
|
+
emit(internal, "close");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const mounts = /* @__PURE__ */ new WeakMap();
|
|
213
|
+
function mount2(opts) {
|
|
214
|
+
if (typeof opts.merchant !== "string" || opts.merchant.indexOf(PUBLISHABLE_KEY_PREFIX) !== 0) {
|
|
215
|
+
try {
|
|
216
|
+
(opts.onEvent || function() {
|
|
217
|
+
})({
|
|
218
|
+
type: "error",
|
|
219
|
+
code: "invalid_merchant_key"
|
|
220
|
+
});
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
return { openNow: function() {
|
|
224
|
+
}, destroy: function() {
|
|
225
|
+
} };
|
|
226
|
+
}
|
|
227
|
+
const originResolution = resolveMountOrigin(opts.canopyOrigin);
|
|
228
|
+
if (!originResolution.ok) {
|
|
229
|
+
try {
|
|
230
|
+
(opts.onEvent || function() {
|
|
231
|
+
})({
|
|
232
|
+
type: "error",
|
|
233
|
+
code: originResolution.code
|
|
234
|
+
});
|
|
235
|
+
} catch {
|
|
236
|
+
}
|
|
237
|
+
return { openNow: function() {
|
|
238
|
+
}, destroy: function() {
|
|
239
|
+
} };
|
|
240
|
+
}
|
|
241
|
+
const node = typeof opts.target === "string" ? document.querySelector(opts.target) : opts.target;
|
|
242
|
+
if (!node) {
|
|
243
|
+
return { openNow: function() {
|
|
244
|
+
}, destroy: function() {
|
|
245
|
+
} };
|
|
246
|
+
}
|
|
247
|
+
const existing = mounts.get(node);
|
|
248
|
+
if (existing) return existing;
|
|
249
|
+
const internal = {
|
|
250
|
+
node,
|
|
251
|
+
capability: opts.capability,
|
|
252
|
+
merchant: opts.merchant,
|
|
253
|
+
origin: originResolution.origin,
|
|
254
|
+
surface: opts.surface || "popup",
|
|
255
|
+
handshakeTimeoutMs: opts.handshakeTimeoutMs || DEFAULT_HANDSHAKE_TIMEOUT_MS,
|
|
256
|
+
fallbackLabel: opts.fallbackLabel || DEFAULT_FALLBACK_LABEL,
|
|
257
|
+
onEvent: opts.onEvent || function() {
|
|
258
|
+
},
|
|
259
|
+
destroyed: false,
|
|
260
|
+
messageListener: function() {
|
|
261
|
+
},
|
|
262
|
+
handshakeTimer: null,
|
|
263
|
+
resizeObserver: null,
|
|
264
|
+
frame: null,
|
|
265
|
+
fallbackButton: null,
|
|
266
|
+
lastHeight: null,
|
|
267
|
+
popupWin: null,
|
|
268
|
+
popupPollTimer: null
|
|
269
|
+
};
|
|
270
|
+
internal.messageListener = function(event) {
|
|
271
|
+
handleMessage(internal, event);
|
|
272
|
+
};
|
|
273
|
+
window.addEventListener("message", internal.messageListener);
|
|
274
|
+
if (internal.surface === "iframe" || internal.surface === "auto") {
|
|
275
|
+
createFrame(internal);
|
|
276
|
+
}
|
|
277
|
+
const handle = {
|
|
278
|
+
openNow: function(win) {
|
|
279
|
+
openNowInternal(internal, win);
|
|
280
|
+
},
|
|
281
|
+
destroy: function() {
|
|
282
|
+
if (internal.destroyed) return;
|
|
283
|
+
internal.destroyed = true;
|
|
284
|
+
window.removeEventListener("message", internal.messageListener);
|
|
285
|
+
teardownFrame(internal);
|
|
286
|
+
if (internal.popupPollTimer) {
|
|
287
|
+
clearInterval(internal.popupPollTimer);
|
|
288
|
+
internal.popupPollTimer = null;
|
|
289
|
+
}
|
|
290
|
+
if (internal.fallbackButton) {
|
|
291
|
+
if (internal.fallbackButton.parentNode) {
|
|
292
|
+
internal.fallbackButton.parentNode.removeChild(
|
|
293
|
+
internal.fallbackButton
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
internal.fallbackButton = null;
|
|
297
|
+
}
|
|
298
|
+
mounts.delete(node);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
mounts.set(node, handle);
|
|
302
|
+
return handle;
|
|
303
|
+
}
|
|
304
|
+
const globalWindow = window;
|
|
305
|
+
if (globalWindow.Canopy && globalWindow.Canopy.__version === LOADER_VERSION) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
globalWindow.Canopy = { mount: mount2, __version: LOADER_VERSION };
|
|
309
|
+
})();
|
|
310
|
+
|
|
311
|
+
// src/index.ts
|
|
312
|
+
function mount(opts) {
|
|
313
|
+
if (typeof window === "undefined" || !window.Canopy) {
|
|
314
|
+
throw new Error("@canopypay/checkout-sdk: loader did not initialise");
|
|
315
|
+
}
|
|
316
|
+
return window.Canopy.mount(opts);
|
|
317
|
+
}
|
|
318
|
+
function version() {
|
|
319
|
+
return typeof window === "undefined" ? void 0 : window.Canopy?.__version;
|
|
320
|
+
}
|
|
321
|
+
export {
|
|
322
|
+
mount,
|
|
323
|
+
version
|
|
324
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@canopypay/checkout-sdk",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"unpkg": "./dist/canopy.global.js",
|
|
10
|
+
"jsdelivr": "./dist/canopy.global.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./canopy.js": "./dist/canopy.global.js",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/index.js",
|
|
22
|
+
"dist/index.cjs",
|
|
23
|
+
"dist/index.d.ts",
|
|
24
|
+
"dist/index.d.cts",
|
|
25
|
+
"dist/canopy.global.js"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup --config tsup.config.ts",
|
|
29
|
+
"build:npm": "tsup --config tsup.npm.config.ts"
|
|
30
|
+
},
|
|
31
|
+
"canopy": {
|
|
32
|
+
"loaderVersion": "2",
|
|
33
|
+
"apiVersion": "2026-09-01"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
}
|
|
38
|
+
}
|