@crediball/elements 0.1.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/chunk-RBGPXBOH.js +341 -0
- package/dist/crediball-elements.iife.js +625 -0
- package/dist/index.d.ts +108 -0
- package/dist/index.js +16 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +17 -0
- package/package.json +65 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// src/base.ts
|
|
2
|
+
import { getCrediballClient } from "@crediball/core";
|
|
3
|
+
var _CrediballElement = class _CrediballElement extends HTMLElement {
|
|
4
|
+
constructor() {
|
|
5
|
+
super(...arguments);
|
|
6
|
+
this.client = null;
|
|
7
|
+
this.unsubscribe = null;
|
|
8
|
+
}
|
|
9
|
+
static get observedAttributes() {
|
|
10
|
+
return [..._CrediballElement.CLIENT_ATTRS];
|
|
11
|
+
}
|
|
12
|
+
connectedCallback() {
|
|
13
|
+
this.connectClient();
|
|
14
|
+
}
|
|
15
|
+
disconnectedCallback() {
|
|
16
|
+
this.teardownClient();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Only reconnect the shared client when a connection-relevant attribute
|
|
20
|
+
* changes (publishable-key/user-id/api-url/low-credit-threshold). Subclasses
|
|
21
|
+
* add their own observed attributes (e.g. "show", "threshold", "open") —
|
|
22
|
+
* those should just re-render, not tear down and refetch the client.
|
|
23
|
+
*/
|
|
24
|
+
attributeChangedCallback(name) {
|
|
25
|
+
if (!this.isConnected) return;
|
|
26
|
+
if (_CrediballElement.CLIENT_ATTRS.has(name)) {
|
|
27
|
+
this.connectClient();
|
|
28
|
+
} else {
|
|
29
|
+
this.render();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
connectClient() {
|
|
33
|
+
this.teardownClient();
|
|
34
|
+
const publishableKey = this.getAttribute("publishable-key");
|
|
35
|
+
const userId = this.getAttribute("user-id");
|
|
36
|
+
if (!publishableKey || !userId) {
|
|
37
|
+
this.render();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const apiUrl = this.getAttribute("api-url") ?? void 0;
|
|
41
|
+
const thresholdAttr = this.getAttribute("low-credit-threshold");
|
|
42
|
+
const lowCreditThreshold = thresholdAttr != null ? Number(thresholdAttr) : void 0;
|
|
43
|
+
this.client = getCrediballClient({ publishableKey, userId, apiUrl, lowCreditThreshold });
|
|
44
|
+
this.client.start();
|
|
45
|
+
this.unsubscribe = this.client.subscribe(() => this.render());
|
|
46
|
+
this.render();
|
|
47
|
+
}
|
|
48
|
+
teardownClient() {
|
|
49
|
+
this.client?.stop();
|
|
50
|
+
this.unsubscribe?.();
|
|
51
|
+
this.unsubscribe = null;
|
|
52
|
+
this.client = null;
|
|
53
|
+
}
|
|
54
|
+
getSnapshot() {
|
|
55
|
+
return this.client?.getSnapshot() ?? null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
_CrediballElement.CLIENT_ATTRS = /* @__PURE__ */ new Set([
|
|
59
|
+
"publishable-key",
|
|
60
|
+
"user-id",
|
|
61
|
+
"api-url",
|
|
62
|
+
"low-credit-threshold"
|
|
63
|
+
]);
|
|
64
|
+
var CrediballElement = _CrediballElement;
|
|
65
|
+
|
|
66
|
+
// src/format.ts
|
|
67
|
+
function formatCredits(credits) {
|
|
68
|
+
return new Intl.NumberFormat(void 0).format(Math.round(credits));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/balance.ts
|
|
72
|
+
var CrediballBalanceElement = class extends CrediballElement {
|
|
73
|
+
constructor() {
|
|
74
|
+
super();
|
|
75
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
76
|
+
shadow.innerHTML = `<style>:host{font:inherit;color:inherit;}</style><span part="value"></span>`;
|
|
77
|
+
this.valueEl = shadow.querySelector("span");
|
|
78
|
+
}
|
|
79
|
+
render() {
|
|
80
|
+
const snapshot = this.getSnapshot();
|
|
81
|
+
this.valueEl.textContent = snapshot?.balance == null ? "\u2014" : formatCredits(snapshot.balance);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/usage.ts
|
|
86
|
+
var CrediballUsageElement = class extends CrediballElement {
|
|
87
|
+
static get observedAttributes() {
|
|
88
|
+
return [...CrediballElement.observedAttributes, "show"];
|
|
89
|
+
}
|
|
90
|
+
constructor() {
|
|
91
|
+
super();
|
|
92
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
93
|
+
shadow.innerHTML = `<style>:host{font:inherit;color:inherit;}</style><span part="value"></span>`;
|
|
94
|
+
this.valueEl = shadow.querySelector("span");
|
|
95
|
+
}
|
|
96
|
+
render() {
|
|
97
|
+
const snapshot = this.getSnapshot();
|
|
98
|
+
const show = this.getAttribute("show") === "remaining" ? "remaining" : "used";
|
|
99
|
+
const value = show === "remaining" ? snapshot?.remaining : snapshot?.usage?.usedCredits ?? null;
|
|
100
|
+
this.valueEl.textContent = value == null ? "\u2014" : formatCredits(value);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/topup-button.ts
|
|
105
|
+
var CrediballTopUpButtonElement = class extends CrediballElement {
|
|
106
|
+
constructor() {
|
|
107
|
+
super();
|
|
108
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
109
|
+
shadow.innerHTML = `
|
|
110
|
+
<style>
|
|
111
|
+
:host { font: inherit; }
|
|
112
|
+
button {
|
|
113
|
+
font: inherit;
|
|
114
|
+
appearance: none;
|
|
115
|
+
border: none;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
color: var(--crediball-on-accent, #ffffff);
|
|
118
|
+
background: var(--crediball-accent, #0066cc);
|
|
119
|
+
border-radius: var(--crediball-radius, 9999px);
|
|
120
|
+
padding: 0.5em 1.1em;
|
|
121
|
+
}
|
|
122
|
+
</style>
|
|
123
|
+
<button part="button" type="button"><slot>Add credits</slot></button>
|
|
124
|
+
`;
|
|
125
|
+
this.button = shadow.querySelector("button");
|
|
126
|
+
this.button.addEventListener("click", () => this.handleClick());
|
|
127
|
+
}
|
|
128
|
+
render() {
|
|
129
|
+
}
|
|
130
|
+
handleClick() {
|
|
131
|
+
const paywall = document.querySelector("crediball-paywall");
|
|
132
|
+
if (paywall?.open) {
|
|
133
|
+
paywall.open();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.dispatchEvent(new CustomEvent("crediball-topup-click", { bubbles: true, composed: true }));
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/low-credit-warning.ts
|
|
141
|
+
var CrediballLowCreditWarningElement = class extends CrediballElement {
|
|
142
|
+
static get observedAttributes() {
|
|
143
|
+
return [...CrediballElement.observedAttributes, "threshold"];
|
|
144
|
+
}
|
|
145
|
+
constructor() {
|
|
146
|
+
super();
|
|
147
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
148
|
+
shadow.innerHTML = `<style>:host{font:inherit;color:inherit;}</style><span part="value"><slot>Low credit balance \u2014 top up to keep going.</slot></span>`;
|
|
149
|
+
}
|
|
150
|
+
render() {
|
|
151
|
+
const snapshot = this.getSnapshot();
|
|
152
|
+
const thresholdAttr = this.getAttribute("threshold");
|
|
153
|
+
const threshold = thresholdAttr != null ? Number(thresholdAttr) : void 0;
|
|
154
|
+
const isLow = threshold != null && snapshot?.balance != null ? snapshot.balance < threshold : !!snapshot?.isLow;
|
|
155
|
+
this.style.display = isLow ? "" : "none";
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/paywall.ts
|
|
160
|
+
function formatMoney(amount, currency) {
|
|
161
|
+
try {
|
|
162
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
|
|
163
|
+
} catch {
|
|
164
|
+
return `${amount.toFixed(2)} ${currency}`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
var HTML_ESCAPES = {
|
|
168
|
+
"&": "&",
|
|
169
|
+
"<": "<",
|
|
170
|
+
">": ">",
|
|
171
|
+
'"': """,
|
|
172
|
+
"'": "'"
|
|
173
|
+
};
|
|
174
|
+
function escapeHtml(s) {
|
|
175
|
+
return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c] ?? c);
|
|
176
|
+
}
|
|
177
|
+
var STYLE = `
|
|
178
|
+
:host { font: var(--crediball-font, system-ui, -apple-system, sans-serif); }
|
|
179
|
+
.overlay {
|
|
180
|
+
display: none;
|
|
181
|
+
position: fixed;
|
|
182
|
+
inset: 0;
|
|
183
|
+
z-index: 2147483647;
|
|
184
|
+
align-items: center;
|
|
185
|
+
justify-content: center;
|
|
186
|
+
padding: 24px;
|
|
187
|
+
background: var(--crediball-overlay, rgba(0, 0, 0, 0.4));
|
|
188
|
+
}
|
|
189
|
+
:host([open]) .overlay { display: flex; }
|
|
190
|
+
.card {
|
|
191
|
+
width: 100%;
|
|
192
|
+
max-width: 420px;
|
|
193
|
+
padding: 32px;
|
|
194
|
+
border-radius: var(--crediball-radius-card, 18px);
|
|
195
|
+
background: var(--crediball-canvas, #ffffff);
|
|
196
|
+
color: var(--crediball-ink, #1d1d1f);
|
|
197
|
+
}
|
|
198
|
+
.balance-row { margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid var(--crediball-hairline, #e0e0e0); }
|
|
199
|
+
.eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--crediball-ink-muted, #7a7a7a); margin-bottom: 4px; }
|
|
200
|
+
.balance-value { font-size: 22px; font-weight: 600; }
|
|
201
|
+
h3 { margin: 0; font-size: 24px; font-weight: 600; letter-spacing: -0.01em; }
|
|
202
|
+
p.desc { margin: 8px 0 0; font-size: 14px; color: var(--crediball-ink-muted, #7a7a7a); }
|
|
203
|
+
.list { display: flex; flex-direction: column; gap: 8px; margin-top: 24px; }
|
|
204
|
+
button.option {
|
|
205
|
+
appearance: none;
|
|
206
|
+
border: none;
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
text-align: left;
|
|
209
|
+
display: flex;
|
|
210
|
+
justify-content: space-between;
|
|
211
|
+
align-items: center;
|
|
212
|
+
background: var(--crediball-accent, #0066cc);
|
|
213
|
+
color: var(--crediball-on-accent, #ffffff);
|
|
214
|
+
font: inherit;
|
|
215
|
+
font-size: 17px;
|
|
216
|
+
padding: 11px 22px;
|
|
217
|
+
border-radius: var(--crediball-radius, 9999px);
|
|
218
|
+
}
|
|
219
|
+
button.option .meta { opacity: 0.85; font-size: 14px; }
|
|
220
|
+
button.close {
|
|
221
|
+
width: 100%;
|
|
222
|
+
margin-top: 12px;
|
|
223
|
+
appearance: none;
|
|
224
|
+
border: none;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
background: transparent;
|
|
227
|
+
color: var(--crediball-ink-muted, #7a7a7a);
|
|
228
|
+
font: inherit;
|
|
229
|
+
font-size: 14px;
|
|
230
|
+
padding: 11px 22px;
|
|
231
|
+
border-radius: var(--crediball-radius, 9999px);
|
|
232
|
+
}
|
|
233
|
+
`;
|
|
234
|
+
var CrediballPaywallElement = class extends CrediballElement {
|
|
235
|
+
static get observedAttributes() {
|
|
236
|
+
return [...CrediballElement.observedAttributes, "open", "title", "description", "amounts"];
|
|
237
|
+
}
|
|
238
|
+
constructor() {
|
|
239
|
+
super();
|
|
240
|
+
this.shadow = this.attachShadow({ mode: "open" });
|
|
241
|
+
}
|
|
242
|
+
open() {
|
|
243
|
+
this.setAttribute("open", "");
|
|
244
|
+
}
|
|
245
|
+
close() {
|
|
246
|
+
this.removeAttribute("open");
|
|
247
|
+
this.dispatchEvent(new CustomEvent("crediball-close", { bubbles: true, composed: true }));
|
|
248
|
+
}
|
|
249
|
+
render() {
|
|
250
|
+
const isOpen = this.hasAttribute("open");
|
|
251
|
+
if (!isOpen) {
|
|
252
|
+
this.shadow.innerHTML = "";
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const snapshot = this.getSnapshot();
|
|
256
|
+
const pkgs = snapshot?.packages;
|
|
257
|
+
const currency = pkgs?.currency ?? "EUR";
|
|
258
|
+
const title = this.getAttribute("title") ?? "You need more credits to continue.";
|
|
259
|
+
const description = this.getAttribute("description") ?? "Top up to keep using this feature.";
|
|
260
|
+
const amountsAttr = this.getAttribute("amounts");
|
|
261
|
+
const legacyAmounts = amountsAttr ? amountsAttr.split(",").map((s) => Number(s.trim())).filter((n) => Number.isFinite(n)) : [5, 10];
|
|
262
|
+
const packages = pkgs?.packages ?? [];
|
|
263
|
+
const plans = pkgs?.activeSubscription ? [] : pkgs?.subscriptionPlans ?? [];
|
|
264
|
+
this.shadow.innerHTML = `
|
|
265
|
+
<style>${STYLE}</style>
|
|
266
|
+
<div class="overlay" part="overlay">
|
|
267
|
+
<div class="card" part="card">
|
|
268
|
+
${snapshot?.balance != null ? `<div class="balance-row">
|
|
269
|
+
<div class="eyebrow">Your balance</div>
|
|
270
|
+
<div class="balance-value">${formatCredits(snapshot.balance)} credits</div>
|
|
271
|
+
</div>` : ""}
|
|
272
|
+
<h3>${escapeHtml(title)}</h3>
|
|
273
|
+
<p class="desc">${escapeHtml(description)}</p>
|
|
274
|
+
${plans.length ? `<div class="list">
|
|
275
|
+
${plans.map(
|
|
276
|
+
(p) => `
|
|
277
|
+
<button class="option" data-kind="plan" data-id="${escapeHtml(p.id)}">
|
|
278
|
+
<span>${escapeHtml(p.name)}</span>
|
|
279
|
+
<span class="meta">${formatMoney(p.price, currency)}/${p.period === "monthly" ? "mo" : "yr"} \xB7 ${formatCredits(p.credits)} cr</span>
|
|
280
|
+
</button>`
|
|
281
|
+
).join("")}
|
|
282
|
+
</div>` : ""}
|
|
283
|
+
<div class="list">
|
|
284
|
+
${packages.length ? packages.map(
|
|
285
|
+
(p) => `
|
|
286
|
+
<button class="option" data-kind="package" data-id="${escapeHtml(p.id)}">
|
|
287
|
+
<span>${escapeHtml(p.name)}</span>
|
|
288
|
+
<span class="meta">${formatMoney(p.price, currency)} \xB7 ${formatCredits(p.credits)} cr</span>
|
|
289
|
+
</button>`
|
|
290
|
+
).join("") : legacyAmounts.map(
|
|
291
|
+
(a) => `
|
|
292
|
+
<button class="option" data-kind="amount" data-amount="${a}">
|
|
293
|
+
<span>Add ${a}</span>
|
|
294
|
+
</button>`
|
|
295
|
+
).join("")}
|
|
296
|
+
</div>
|
|
297
|
+
<button class="close" part="close" type="button">Not now</button>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
`;
|
|
301
|
+
this.shadow.querySelector(".overlay")?.addEventListener("click", (e) => {
|
|
302
|
+
if (e.target === e.currentTarget) this.close();
|
|
303
|
+
});
|
|
304
|
+
this.shadow.querySelector(".close")?.addEventListener("click", () => this.close());
|
|
305
|
+
this.shadow.querySelectorAll("button.option").forEach((el) => {
|
|
306
|
+
const btn = el;
|
|
307
|
+
btn.addEventListener("click", () => {
|
|
308
|
+
const kind = btn.dataset.kind;
|
|
309
|
+
if (kind === "package") {
|
|
310
|
+
const pkg = packages.find((p) => p.id === btn.dataset.id);
|
|
311
|
+
if (pkg) {
|
|
312
|
+
this.dispatchEvent(
|
|
313
|
+
new CustomEvent("crediball-select-package", { detail: pkg, bubbles: true, composed: true })
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
} else if (kind === "plan") {
|
|
317
|
+
const plan = plans.find((p) => p.id === btn.dataset.id);
|
|
318
|
+
if (plan) {
|
|
319
|
+
this.dispatchEvent(
|
|
320
|
+
new CustomEvent("crediball-select-plan", { detail: plan, bubbles: true, composed: true })
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
} else if (kind === "amount") {
|
|
324
|
+
const amount = Number(btn.dataset.amount);
|
|
325
|
+
this.dispatchEvent(
|
|
326
|
+
new CustomEvent("crediball-topup", { detail: { amount }, bubbles: true, composed: true })
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export {
|
|
335
|
+
CrediballElement,
|
|
336
|
+
CrediballBalanceElement,
|
|
337
|
+
CrediballUsageElement,
|
|
338
|
+
CrediballTopUpButtonElement,
|
|
339
|
+
CrediballLowCreditWarningElement,
|
|
340
|
+
CrediballPaywallElement
|
|
341
|
+
};
|
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var Crediball = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/iife.ts
|
|
22
|
+
var iife_exports = {};
|
|
23
|
+
__export(iife_exports, {
|
|
24
|
+
CrediballBalanceElement: () => CrediballBalanceElement,
|
|
25
|
+
CrediballElement: () => CrediballElement,
|
|
26
|
+
CrediballLowCreditWarningElement: () => CrediballLowCreditWarningElement,
|
|
27
|
+
CrediballPaywallElement: () => CrediballPaywallElement,
|
|
28
|
+
CrediballTopUpButtonElement: () => CrediballTopUpButtonElement,
|
|
29
|
+
CrediballUsageElement: () => CrediballUsageElement
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ../core/dist/store.js
|
|
33
|
+
function createStore(initial) {
|
|
34
|
+
let snapshot = initial;
|
|
35
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
36
|
+
return {
|
|
37
|
+
get: () => snapshot,
|
|
38
|
+
set(next) {
|
|
39
|
+
snapshot = next;
|
|
40
|
+
listeners.forEach((fn) => fn());
|
|
41
|
+
},
|
|
42
|
+
update(fn) {
|
|
43
|
+
snapshot = fn(snapshot);
|
|
44
|
+
listeners.forEach((listener) => listener());
|
|
45
|
+
},
|
|
46
|
+
subscribe(listener) {
|
|
47
|
+
listeners.add(listener);
|
|
48
|
+
return () => listeners.delete(listener);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ../core/dist/events.js
|
|
54
|
+
var INSUFFICIENT_CREDITS_EVENT = "crediball:insufficient-credits";
|
|
55
|
+
var BALANCE_CHANGED_EVENT = "crediball:balance-changed";
|
|
56
|
+
function subscribeInsufficientCredits(listener) {
|
|
57
|
+
if (typeof window === "undefined")
|
|
58
|
+
return () => {
|
|
59
|
+
};
|
|
60
|
+
window.addEventListener(INSUFFICIENT_CREDITS_EVENT, listener);
|
|
61
|
+
return () => window.removeEventListener(INSUFFICIENT_CREDITS_EVENT, listener);
|
|
62
|
+
}
|
|
63
|
+
function subscribeBalanceChanged(listener) {
|
|
64
|
+
if (typeof window === "undefined")
|
|
65
|
+
return () => {
|
|
66
|
+
};
|
|
67
|
+
window.addEventListener(BALANCE_CHANGED_EVENT, listener);
|
|
68
|
+
return () => window.removeEventListener(BALANCE_CHANGED_EVENT, listener);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ../core/dist/client.js
|
|
72
|
+
var DEFAULT_API_URL = "https://crediball.app/api";
|
|
73
|
+
var DEFAULT_POLL_MS = 3e4;
|
|
74
|
+
var SSE_HEALTHY_POLL_MULTIPLIER = 2;
|
|
75
|
+
var MAX_SSE_FAILURES = 2;
|
|
76
|
+
function emptySnapshot() {
|
|
77
|
+
return {
|
|
78
|
+
balance: null,
|
|
79
|
+
usage: null,
|
|
80
|
+
remaining: null,
|
|
81
|
+
costs: {},
|
|
82
|
+
packages: null,
|
|
83
|
+
isLow: false,
|
|
84
|
+
loading: true,
|
|
85
|
+
error: null
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function fetchJson(url, headers) {
|
|
89
|
+
const res = await fetch(url, { headers });
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
const body = await res.json().catch(() => null);
|
|
92
|
+
const message = body && typeof body === "object" && "error" in body && String(body.error) || `Request to ${url} failed with ${res.status}`;
|
|
93
|
+
throw new Error(message);
|
|
94
|
+
}
|
|
95
|
+
return res.json();
|
|
96
|
+
}
|
|
97
|
+
function computeIsLow(balance, costs, explicitThreshold) {
|
|
98
|
+
if (balance == null)
|
|
99
|
+
return false;
|
|
100
|
+
const threshold = explicitThreshold ?? Object.values(costs).reduce((max, c) => Math.max(max, c.costCredits), 0);
|
|
101
|
+
if (threshold <= 0)
|
|
102
|
+
return false;
|
|
103
|
+
return balance < threshold;
|
|
104
|
+
}
|
|
105
|
+
var CrediballClientImpl = class {
|
|
106
|
+
constructor(config, onDestroy) {
|
|
107
|
+
this.config = config;
|
|
108
|
+
this.onDestroy = onDestroy;
|
|
109
|
+
this.store = createStore(emptySnapshot());
|
|
110
|
+
this.refCount = 0;
|
|
111
|
+
this.pollTimer = null;
|
|
112
|
+
this.eventSource = null;
|
|
113
|
+
this.sseFailures = 0;
|
|
114
|
+
this.sseHealthy = false;
|
|
115
|
+
this.teardownListeners = [];
|
|
116
|
+
this.getSnapshot = () => {
|
|
117
|
+
return this.store.get();
|
|
118
|
+
};
|
|
119
|
+
this.subscribe = (fn) => {
|
|
120
|
+
return this.store.subscribe(fn);
|
|
121
|
+
};
|
|
122
|
+
this.costOf = (event) => {
|
|
123
|
+
return this.store.get().costs[event]?.costCredits;
|
|
124
|
+
};
|
|
125
|
+
this.refresh = async () => {
|
|
126
|
+
const { publishableKey, userId } = this.config;
|
|
127
|
+
const headers = { Authorization: `Bearer ${publishableKey}` };
|
|
128
|
+
const qs = `userId=${encodeURIComponent(userId)}`;
|
|
129
|
+
try {
|
|
130
|
+
const [balanceRes, usageRes, costsRes, packagesRes] = await Promise.all([
|
|
131
|
+
fetchJson(`${this.apiUrl}/public/balance?${qs}`, headers),
|
|
132
|
+
fetchJson(`${this.apiUrl}/public/usage?${qs}`, headers),
|
|
133
|
+
fetchJson(`${this.apiUrl}/public/costs`, headers),
|
|
134
|
+
fetchJson(`${this.apiUrl}/public/packages?${qs}`, headers)
|
|
135
|
+
]);
|
|
136
|
+
const costs = {};
|
|
137
|
+
for (const e of costsRes.events)
|
|
138
|
+
costs[e.key] = { label: e.label, costCredits: e.costCredits };
|
|
139
|
+
const balance = balanceRes.balance;
|
|
140
|
+
this.store.set({
|
|
141
|
+
balance,
|
|
142
|
+
usage: usageRes,
|
|
143
|
+
remaining: balance,
|
|
144
|
+
costs,
|
|
145
|
+
packages: packagesRes,
|
|
146
|
+
isLow: computeIsLow(balance, costs, this.config.lowCreditThreshold),
|
|
147
|
+
loading: false,
|
|
148
|
+
error: null
|
|
149
|
+
});
|
|
150
|
+
} catch (err) {
|
|
151
|
+
this.store.update((prev) => ({
|
|
152
|
+
...prev,
|
|
153
|
+
loading: false,
|
|
154
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
this.start = () => {
|
|
159
|
+
this.refCount += 1;
|
|
160
|
+
if (this.refCount > 1)
|
|
161
|
+
return;
|
|
162
|
+
void this.refresh();
|
|
163
|
+
this.startPolling();
|
|
164
|
+
if ((this.config.realtime ?? "sse") !== "off")
|
|
165
|
+
this.connectSse();
|
|
166
|
+
this.teardownListeners = [
|
|
167
|
+
subscribeInsufficientCredits(() => void this.refresh()),
|
|
168
|
+
subscribeBalanceChanged(() => void this.refresh()),
|
|
169
|
+
...this.visibilityListeners()
|
|
170
|
+
];
|
|
171
|
+
};
|
|
172
|
+
this.stop = () => {
|
|
173
|
+
this.refCount = Math.max(0, this.refCount - 1);
|
|
174
|
+
if (this.refCount === 0)
|
|
175
|
+
this.pause();
|
|
176
|
+
};
|
|
177
|
+
this.destroy = () => {
|
|
178
|
+
this.refCount = 0;
|
|
179
|
+
this.pause();
|
|
180
|
+
this.onDestroy();
|
|
181
|
+
};
|
|
182
|
+
this.apiUrl = (config.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
183
|
+
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_MS;
|
|
184
|
+
}
|
|
185
|
+
pause() {
|
|
186
|
+
this.stopPolling();
|
|
187
|
+
this.disconnectSse();
|
|
188
|
+
this.teardownListeners.forEach((fn) => fn());
|
|
189
|
+
this.teardownListeners = [];
|
|
190
|
+
}
|
|
191
|
+
effectivePollIntervalMs() {
|
|
192
|
+
return this.sseHealthy ? this.pollIntervalMs * SSE_HEALTHY_POLL_MULTIPLIER : this.pollIntervalMs;
|
|
193
|
+
}
|
|
194
|
+
startPolling() {
|
|
195
|
+
this.stopPolling();
|
|
196
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden")
|
|
197
|
+
return;
|
|
198
|
+
this.pollTimer = setInterval(() => void this.refresh(), this.effectivePollIntervalMs());
|
|
199
|
+
}
|
|
200
|
+
stopPolling() {
|
|
201
|
+
if (this.pollTimer) {
|
|
202
|
+
clearInterval(this.pollTimer);
|
|
203
|
+
this.pollTimer = null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
visibilityListeners() {
|
|
207
|
+
if (typeof document === "undefined" || typeof window === "undefined")
|
|
208
|
+
return [];
|
|
209
|
+
const onVisibilityChange = () => {
|
|
210
|
+
if (document.visibilityState === "hidden") {
|
|
211
|
+
this.stopPolling();
|
|
212
|
+
} else {
|
|
213
|
+
void this.refresh();
|
|
214
|
+
this.startPolling();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const onOnline = () => void this.refresh();
|
|
218
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
219
|
+
window.addEventListener("online", onOnline);
|
|
220
|
+
return [
|
|
221
|
+
() => document.removeEventListener("visibilitychange", onVisibilityChange),
|
|
222
|
+
() => window.removeEventListener("online", onOnline)
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
connectSse() {
|
|
226
|
+
if (typeof EventSource === "undefined")
|
|
227
|
+
return;
|
|
228
|
+
const { publishableKey, userId } = this.config;
|
|
229
|
+
const url = `${this.apiUrl}/public/stream?key=${encodeURIComponent(publishableKey)}&userId=${encodeURIComponent(userId)}`;
|
|
230
|
+
const es = new EventSource(url);
|
|
231
|
+
this.eventSource = es;
|
|
232
|
+
es.addEventListener("update", (ev) => {
|
|
233
|
+
this.sseFailures = 0;
|
|
234
|
+
if (!this.sseHealthy) {
|
|
235
|
+
this.sseHealthy = true;
|
|
236
|
+
this.startPolling();
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const data = JSON.parse(ev.data);
|
|
240
|
+
this.store.update((prev) => {
|
|
241
|
+
const balance = data.balance ?? prev.balance;
|
|
242
|
+
const usage = data.usage ?? prev.usage;
|
|
243
|
+
return {
|
|
244
|
+
...prev,
|
|
245
|
+
balance,
|
|
246
|
+
usage,
|
|
247
|
+
remaining: balance,
|
|
248
|
+
isLow: computeIsLow(balance, prev.costs, this.config.lowCreditThreshold)
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
es.onerror = () => {
|
|
255
|
+
this.sseFailures += 1;
|
|
256
|
+
this.sseHealthy = false;
|
|
257
|
+
this.startPolling();
|
|
258
|
+
if (this.sseFailures >= MAX_SSE_FAILURES) {
|
|
259
|
+
this.disconnectSse();
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
disconnectSse() {
|
|
264
|
+
this.eventSource?.close();
|
|
265
|
+
this.eventSource = null;
|
|
266
|
+
this.sseHealthy = false;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var registry = /* @__PURE__ */ new Map();
|
|
270
|
+
function registryKey(config) {
|
|
271
|
+
return `${config.apiUrl ?? DEFAULT_API_URL}::${config.publishableKey}::${config.userId}`;
|
|
272
|
+
}
|
|
273
|
+
function getCrediballClient(config) {
|
|
274
|
+
const key = registryKey(config);
|
|
275
|
+
const existing = registry.get(key);
|
|
276
|
+
if (existing)
|
|
277
|
+
return existing;
|
|
278
|
+
const client = new CrediballClientImpl(config, () => registry.delete(key));
|
|
279
|
+
registry.set(key, client);
|
|
280
|
+
return client;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/base.ts
|
|
284
|
+
var _CrediballElement = class _CrediballElement extends HTMLElement {
|
|
285
|
+
constructor() {
|
|
286
|
+
super(...arguments);
|
|
287
|
+
this.client = null;
|
|
288
|
+
this.unsubscribe = null;
|
|
289
|
+
}
|
|
290
|
+
static get observedAttributes() {
|
|
291
|
+
return [..._CrediballElement.CLIENT_ATTRS];
|
|
292
|
+
}
|
|
293
|
+
connectedCallback() {
|
|
294
|
+
this.connectClient();
|
|
295
|
+
}
|
|
296
|
+
disconnectedCallback() {
|
|
297
|
+
this.teardownClient();
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Only reconnect the shared client when a connection-relevant attribute
|
|
301
|
+
* changes (publishable-key/user-id/api-url/low-credit-threshold). Subclasses
|
|
302
|
+
* add their own observed attributes (e.g. "show", "threshold", "open") —
|
|
303
|
+
* those should just re-render, not tear down and refetch the client.
|
|
304
|
+
*/
|
|
305
|
+
attributeChangedCallback(name) {
|
|
306
|
+
if (!this.isConnected) return;
|
|
307
|
+
if (_CrediballElement.CLIENT_ATTRS.has(name)) {
|
|
308
|
+
this.connectClient();
|
|
309
|
+
} else {
|
|
310
|
+
this.render();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
connectClient() {
|
|
314
|
+
this.teardownClient();
|
|
315
|
+
const publishableKey = this.getAttribute("publishable-key");
|
|
316
|
+
const userId = this.getAttribute("user-id");
|
|
317
|
+
if (!publishableKey || !userId) {
|
|
318
|
+
this.render();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const apiUrl = this.getAttribute("api-url") ?? void 0;
|
|
322
|
+
const thresholdAttr = this.getAttribute("low-credit-threshold");
|
|
323
|
+
const lowCreditThreshold = thresholdAttr != null ? Number(thresholdAttr) : void 0;
|
|
324
|
+
this.client = getCrediballClient({ publishableKey, userId, apiUrl, lowCreditThreshold });
|
|
325
|
+
this.client.start();
|
|
326
|
+
this.unsubscribe = this.client.subscribe(() => this.render());
|
|
327
|
+
this.render();
|
|
328
|
+
}
|
|
329
|
+
teardownClient() {
|
|
330
|
+
this.client?.stop();
|
|
331
|
+
this.unsubscribe?.();
|
|
332
|
+
this.unsubscribe = null;
|
|
333
|
+
this.client = null;
|
|
334
|
+
}
|
|
335
|
+
getSnapshot() {
|
|
336
|
+
return this.client?.getSnapshot() ?? null;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
_CrediballElement.CLIENT_ATTRS = /* @__PURE__ */ new Set([
|
|
340
|
+
"publishable-key",
|
|
341
|
+
"user-id",
|
|
342
|
+
"api-url",
|
|
343
|
+
"low-credit-threshold"
|
|
344
|
+
]);
|
|
345
|
+
var CrediballElement = _CrediballElement;
|
|
346
|
+
|
|
347
|
+
// src/format.ts
|
|
348
|
+
function formatCredits(credits) {
|
|
349
|
+
return new Intl.NumberFormat(void 0).format(Math.round(credits));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/balance.ts
|
|
353
|
+
var CrediballBalanceElement = class extends CrediballElement {
|
|
354
|
+
constructor() {
|
|
355
|
+
super();
|
|
356
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
357
|
+
shadow.innerHTML = `<style>:host{font:inherit;color:inherit;}</style><span part="value"></span>`;
|
|
358
|
+
this.valueEl = shadow.querySelector("span");
|
|
359
|
+
}
|
|
360
|
+
render() {
|
|
361
|
+
const snapshot = this.getSnapshot();
|
|
362
|
+
this.valueEl.textContent = snapshot?.balance == null ? "\u2014" : formatCredits(snapshot.balance);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// src/usage.ts
|
|
367
|
+
var CrediballUsageElement = class extends CrediballElement {
|
|
368
|
+
static get observedAttributes() {
|
|
369
|
+
return [...CrediballElement.observedAttributes, "show"];
|
|
370
|
+
}
|
|
371
|
+
constructor() {
|
|
372
|
+
super();
|
|
373
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
374
|
+
shadow.innerHTML = `<style>:host{font:inherit;color:inherit;}</style><span part="value"></span>`;
|
|
375
|
+
this.valueEl = shadow.querySelector("span");
|
|
376
|
+
}
|
|
377
|
+
render() {
|
|
378
|
+
const snapshot = this.getSnapshot();
|
|
379
|
+
const show = this.getAttribute("show") === "remaining" ? "remaining" : "used";
|
|
380
|
+
const value = show === "remaining" ? snapshot?.remaining : snapshot?.usage?.usedCredits ?? null;
|
|
381
|
+
this.valueEl.textContent = value == null ? "\u2014" : formatCredits(value);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// src/topup-button.ts
|
|
386
|
+
var CrediballTopUpButtonElement = class extends CrediballElement {
|
|
387
|
+
constructor() {
|
|
388
|
+
super();
|
|
389
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
390
|
+
shadow.innerHTML = `
|
|
391
|
+
<style>
|
|
392
|
+
:host { font: inherit; }
|
|
393
|
+
button {
|
|
394
|
+
font: inherit;
|
|
395
|
+
appearance: none;
|
|
396
|
+
border: none;
|
|
397
|
+
cursor: pointer;
|
|
398
|
+
color: var(--crediball-on-accent, #ffffff);
|
|
399
|
+
background: var(--crediball-accent, #0066cc);
|
|
400
|
+
border-radius: var(--crediball-radius, 9999px);
|
|
401
|
+
padding: 0.5em 1.1em;
|
|
402
|
+
}
|
|
403
|
+
</style>
|
|
404
|
+
<button part="button" type="button"><slot>Add credits</slot></button>
|
|
405
|
+
`;
|
|
406
|
+
this.button = shadow.querySelector("button");
|
|
407
|
+
this.button.addEventListener("click", () => this.handleClick());
|
|
408
|
+
}
|
|
409
|
+
render() {
|
|
410
|
+
}
|
|
411
|
+
handleClick() {
|
|
412
|
+
const paywall = document.querySelector("crediball-paywall");
|
|
413
|
+
if (paywall?.open) {
|
|
414
|
+
paywall.open();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
this.dispatchEvent(new CustomEvent("crediball-topup-click", { bubbles: true, composed: true }));
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// src/low-credit-warning.ts
|
|
422
|
+
var CrediballLowCreditWarningElement = class extends CrediballElement {
|
|
423
|
+
static get observedAttributes() {
|
|
424
|
+
return [...CrediballElement.observedAttributes, "threshold"];
|
|
425
|
+
}
|
|
426
|
+
constructor() {
|
|
427
|
+
super();
|
|
428
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
429
|
+
shadow.innerHTML = `<style>:host{font:inherit;color:inherit;}</style><span part="value"><slot>Low credit balance \u2014 top up to keep going.</slot></span>`;
|
|
430
|
+
}
|
|
431
|
+
render() {
|
|
432
|
+
const snapshot = this.getSnapshot();
|
|
433
|
+
const thresholdAttr = this.getAttribute("threshold");
|
|
434
|
+
const threshold = thresholdAttr != null ? Number(thresholdAttr) : void 0;
|
|
435
|
+
const isLow = threshold != null && snapshot?.balance != null ? snapshot.balance < threshold : !!snapshot?.isLow;
|
|
436
|
+
this.style.display = isLow ? "" : "none";
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// src/paywall.ts
|
|
441
|
+
function formatMoney(amount, currency) {
|
|
442
|
+
try {
|
|
443
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
|
|
444
|
+
} catch {
|
|
445
|
+
return `${amount.toFixed(2)} ${currency}`;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
var HTML_ESCAPES = {
|
|
449
|
+
"&": "&",
|
|
450
|
+
"<": "<",
|
|
451
|
+
">": ">",
|
|
452
|
+
'"': """,
|
|
453
|
+
"'": "'"
|
|
454
|
+
};
|
|
455
|
+
function escapeHtml(s) {
|
|
456
|
+
return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c] ?? c);
|
|
457
|
+
}
|
|
458
|
+
var STYLE = `
|
|
459
|
+
:host { font: var(--crediball-font, system-ui, -apple-system, sans-serif); }
|
|
460
|
+
.overlay {
|
|
461
|
+
display: none;
|
|
462
|
+
position: fixed;
|
|
463
|
+
inset: 0;
|
|
464
|
+
z-index: 2147483647;
|
|
465
|
+
align-items: center;
|
|
466
|
+
justify-content: center;
|
|
467
|
+
padding: 24px;
|
|
468
|
+
background: var(--crediball-overlay, rgba(0, 0, 0, 0.4));
|
|
469
|
+
}
|
|
470
|
+
:host([open]) .overlay { display: flex; }
|
|
471
|
+
.card {
|
|
472
|
+
width: 100%;
|
|
473
|
+
max-width: 420px;
|
|
474
|
+
padding: 32px;
|
|
475
|
+
border-radius: var(--crediball-radius-card, 18px);
|
|
476
|
+
background: var(--crediball-canvas, #ffffff);
|
|
477
|
+
color: var(--crediball-ink, #1d1d1f);
|
|
478
|
+
}
|
|
479
|
+
.balance-row { margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid var(--crediball-hairline, #e0e0e0); }
|
|
480
|
+
.eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--crediball-ink-muted, #7a7a7a); margin-bottom: 4px; }
|
|
481
|
+
.balance-value { font-size: 22px; font-weight: 600; }
|
|
482
|
+
h3 { margin: 0; font-size: 24px; font-weight: 600; letter-spacing: -0.01em; }
|
|
483
|
+
p.desc { margin: 8px 0 0; font-size: 14px; color: var(--crediball-ink-muted, #7a7a7a); }
|
|
484
|
+
.list { display: flex; flex-direction: column; gap: 8px; margin-top: 24px; }
|
|
485
|
+
button.option {
|
|
486
|
+
appearance: none;
|
|
487
|
+
border: none;
|
|
488
|
+
cursor: pointer;
|
|
489
|
+
text-align: left;
|
|
490
|
+
display: flex;
|
|
491
|
+
justify-content: space-between;
|
|
492
|
+
align-items: center;
|
|
493
|
+
background: var(--crediball-accent, #0066cc);
|
|
494
|
+
color: var(--crediball-on-accent, #ffffff);
|
|
495
|
+
font: inherit;
|
|
496
|
+
font-size: 17px;
|
|
497
|
+
padding: 11px 22px;
|
|
498
|
+
border-radius: var(--crediball-radius, 9999px);
|
|
499
|
+
}
|
|
500
|
+
button.option .meta { opacity: 0.85; font-size: 14px; }
|
|
501
|
+
button.close {
|
|
502
|
+
width: 100%;
|
|
503
|
+
margin-top: 12px;
|
|
504
|
+
appearance: none;
|
|
505
|
+
border: none;
|
|
506
|
+
cursor: pointer;
|
|
507
|
+
background: transparent;
|
|
508
|
+
color: var(--crediball-ink-muted, #7a7a7a);
|
|
509
|
+
font: inherit;
|
|
510
|
+
font-size: 14px;
|
|
511
|
+
padding: 11px 22px;
|
|
512
|
+
border-radius: var(--crediball-radius, 9999px);
|
|
513
|
+
}
|
|
514
|
+
`;
|
|
515
|
+
var CrediballPaywallElement = class extends CrediballElement {
|
|
516
|
+
static get observedAttributes() {
|
|
517
|
+
return [...CrediballElement.observedAttributes, "open", "title", "description", "amounts"];
|
|
518
|
+
}
|
|
519
|
+
constructor() {
|
|
520
|
+
super();
|
|
521
|
+
this.shadow = this.attachShadow({ mode: "open" });
|
|
522
|
+
}
|
|
523
|
+
open() {
|
|
524
|
+
this.setAttribute("open", "");
|
|
525
|
+
}
|
|
526
|
+
close() {
|
|
527
|
+
this.removeAttribute("open");
|
|
528
|
+
this.dispatchEvent(new CustomEvent("crediball-close", { bubbles: true, composed: true }));
|
|
529
|
+
}
|
|
530
|
+
render() {
|
|
531
|
+
const isOpen = this.hasAttribute("open");
|
|
532
|
+
if (!isOpen) {
|
|
533
|
+
this.shadow.innerHTML = "";
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const snapshot = this.getSnapshot();
|
|
537
|
+
const pkgs = snapshot?.packages;
|
|
538
|
+
const currency = pkgs?.currency ?? "EUR";
|
|
539
|
+
const title = this.getAttribute("title") ?? "You need more credits to continue.";
|
|
540
|
+
const description = this.getAttribute("description") ?? "Top up to keep using this feature.";
|
|
541
|
+
const amountsAttr = this.getAttribute("amounts");
|
|
542
|
+
const legacyAmounts = amountsAttr ? amountsAttr.split(",").map((s) => Number(s.trim())).filter((n) => Number.isFinite(n)) : [5, 10];
|
|
543
|
+
const packages = pkgs?.packages ?? [];
|
|
544
|
+
const plans = pkgs?.activeSubscription ? [] : pkgs?.subscriptionPlans ?? [];
|
|
545
|
+
this.shadow.innerHTML = `
|
|
546
|
+
<style>${STYLE}</style>
|
|
547
|
+
<div class="overlay" part="overlay">
|
|
548
|
+
<div class="card" part="card">
|
|
549
|
+
${snapshot?.balance != null ? `<div class="balance-row">
|
|
550
|
+
<div class="eyebrow">Your balance</div>
|
|
551
|
+
<div class="balance-value">${formatCredits(snapshot.balance)} credits</div>
|
|
552
|
+
</div>` : ""}
|
|
553
|
+
<h3>${escapeHtml(title)}</h3>
|
|
554
|
+
<p class="desc">${escapeHtml(description)}</p>
|
|
555
|
+
${plans.length ? `<div class="list">
|
|
556
|
+
${plans.map(
|
|
557
|
+
(p) => `
|
|
558
|
+
<button class="option" data-kind="plan" data-id="${escapeHtml(p.id)}">
|
|
559
|
+
<span>${escapeHtml(p.name)}</span>
|
|
560
|
+
<span class="meta">${formatMoney(p.price, currency)}/${p.period === "monthly" ? "mo" : "yr"} \xB7 ${formatCredits(p.credits)} cr</span>
|
|
561
|
+
</button>`
|
|
562
|
+
).join("")}
|
|
563
|
+
</div>` : ""}
|
|
564
|
+
<div class="list">
|
|
565
|
+
${packages.length ? packages.map(
|
|
566
|
+
(p) => `
|
|
567
|
+
<button class="option" data-kind="package" data-id="${escapeHtml(p.id)}">
|
|
568
|
+
<span>${escapeHtml(p.name)}</span>
|
|
569
|
+
<span class="meta">${formatMoney(p.price, currency)} \xB7 ${formatCredits(p.credits)} cr</span>
|
|
570
|
+
</button>`
|
|
571
|
+
).join("") : legacyAmounts.map(
|
|
572
|
+
(a) => `
|
|
573
|
+
<button class="option" data-kind="amount" data-amount="${a}">
|
|
574
|
+
<span>Add ${a}</span>
|
|
575
|
+
</button>`
|
|
576
|
+
).join("")}
|
|
577
|
+
</div>
|
|
578
|
+
<button class="close" part="close" type="button">Not now</button>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
`;
|
|
582
|
+
this.shadow.querySelector(".overlay")?.addEventListener("click", (e) => {
|
|
583
|
+
if (e.target === e.currentTarget) this.close();
|
|
584
|
+
});
|
|
585
|
+
this.shadow.querySelector(".close")?.addEventListener("click", () => this.close());
|
|
586
|
+
this.shadow.querySelectorAll("button.option").forEach((el) => {
|
|
587
|
+
const btn = el;
|
|
588
|
+
btn.addEventListener("click", () => {
|
|
589
|
+
const kind = btn.dataset.kind;
|
|
590
|
+
if (kind === "package") {
|
|
591
|
+
const pkg = packages.find((p) => p.id === btn.dataset.id);
|
|
592
|
+
if (pkg) {
|
|
593
|
+
this.dispatchEvent(
|
|
594
|
+
new CustomEvent("crediball-select-package", { detail: pkg, bubbles: true, composed: true })
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
} else if (kind === "plan") {
|
|
598
|
+
const plan = plans.find((p) => p.id === btn.dataset.id);
|
|
599
|
+
if (plan) {
|
|
600
|
+
this.dispatchEvent(
|
|
601
|
+
new CustomEvent("crediball-select-plan", { detail: plan, bubbles: true, composed: true })
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
} else if (kind === "amount") {
|
|
605
|
+
const amount = Number(btn.dataset.amount);
|
|
606
|
+
this.dispatchEvent(
|
|
607
|
+
new CustomEvent("crediball-topup", { detail: { amount }, bubbles: true, composed: true })
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// src/register.ts
|
|
616
|
+
function define(name, ctor) {
|
|
617
|
+
if (!customElements.get(name)) customElements.define(name, ctor);
|
|
618
|
+
}
|
|
619
|
+
define("crediball-balance", CrediballBalanceElement);
|
|
620
|
+
define("crediball-usage", CrediballUsageElement);
|
|
621
|
+
define("crediball-topup-button", CrediballTopUpButtonElement);
|
|
622
|
+
define("crediball-low-credit-warning", CrediballLowCreditWarningElement);
|
|
623
|
+
define("crediball-paywall", CrediballPaywallElement);
|
|
624
|
+
return __toCommonJS(iife_exports);
|
|
625
|
+
})();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { CrediballClient, CrediballSnapshot } from '@crediball/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared base for every <crediball-*> element: reads config from attributes,
|
|
5
|
+
* gets (or reuses) the shared CrediballClient for that publishable-key +
|
|
6
|
+
* user-id, and re-renders on every snapshot change. Multiple elements on a
|
|
7
|
+
* page with the same publishable-key/user-id/api-url share one poller —
|
|
8
|
+
* see getCrediballClient() in @crediball/core.
|
|
9
|
+
*
|
|
10
|
+
* <crediball-balance publishable-key="cb_pub_..." user-id="u123"></crediball-balance>
|
|
11
|
+
*/
|
|
12
|
+
declare abstract class CrediballElement extends HTMLElement {
|
|
13
|
+
protected client: CrediballClient | null;
|
|
14
|
+
private unsubscribe;
|
|
15
|
+
private static readonly CLIENT_ATTRS;
|
|
16
|
+
static get observedAttributes(): string[];
|
|
17
|
+
connectedCallback(): void;
|
|
18
|
+
disconnectedCallback(): void;
|
|
19
|
+
/**
|
|
20
|
+
* Only reconnect the shared client when a connection-relevant attribute
|
|
21
|
+
* changes (publishable-key/user-id/api-url/low-credit-threshold). Subclasses
|
|
22
|
+
* add their own observed attributes (e.g. "show", "threshold", "open") —
|
|
23
|
+
* those should just re-render, not tear down and refetch the client.
|
|
24
|
+
*/
|
|
25
|
+
attributeChangedCallback(name: string): void;
|
|
26
|
+
private connectClient;
|
|
27
|
+
private teardownClient;
|
|
28
|
+
protected getSnapshot(): CrediballSnapshot | null;
|
|
29
|
+
/** Called on connect, on relevant attribute changes, and on every data update. */
|
|
30
|
+
protected abstract render(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* <crediball-balance publishable-key="cb_pub_..." user-id="u123"></crediball-balance>
|
|
35
|
+
*
|
|
36
|
+
* Inherits the surrounding font and color (`:host { font: inherit; color: inherit }`)
|
|
37
|
+
* so it looks native wherever it's placed.
|
|
38
|
+
*/
|
|
39
|
+
declare class CrediballBalanceElement extends CrediballElement {
|
|
40
|
+
private valueEl;
|
|
41
|
+
constructor();
|
|
42
|
+
protected render(): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* <crediball-usage publishable-key="cb_pub_..." user-id="u123"></crediball-usage>
|
|
47
|
+
*
|
|
48
|
+
* Credits consumed in the user's current period by default. Set
|
|
49
|
+
* `show="remaining"` to render the balance instead.
|
|
50
|
+
*/
|
|
51
|
+
declare class CrediballUsageElement extends CrediballElement {
|
|
52
|
+
private valueEl;
|
|
53
|
+
static get observedAttributes(): string[];
|
|
54
|
+
constructor();
|
|
55
|
+
protected render(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* <crediball-topup-button publishable-key="cb_pub_..." user-id="u123">
|
|
60
|
+
* Add credits
|
|
61
|
+
* </crediball-topup-button>
|
|
62
|
+
*
|
|
63
|
+
* Clicking it opens the nearest <crediball-paywall> in the document. If none
|
|
64
|
+
* is present, it dispatches a bubbling `crediball-topup-click` event so the
|
|
65
|
+
* host page can handle it itself.
|
|
66
|
+
*/
|
|
67
|
+
declare class CrediballTopUpButtonElement extends CrediballElement {
|
|
68
|
+
private button;
|
|
69
|
+
constructor();
|
|
70
|
+
protected render(): void;
|
|
71
|
+
private handleClick;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* <crediball-low-credit-warning publishable-key="cb_pub_..." user-id="u123">
|
|
76
|
+
* Low credit balance — top up to keep going.
|
|
77
|
+
* </crediball-low-credit-warning>
|
|
78
|
+
*
|
|
79
|
+
* Hidden until the user's balance drops below the threshold (the highest-cost
|
|
80
|
+
* active event by default, or the `threshold` attribute).
|
|
81
|
+
*/
|
|
82
|
+
declare class CrediballLowCreditWarningElement extends CrediballElement {
|
|
83
|
+
static get observedAttributes(): string[];
|
|
84
|
+
constructor();
|
|
85
|
+
protected render(): void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* <crediball-paywall publishable-key="cb_pub_..." user-id="u123" title="..." description="..."></crediball-paywall>
|
|
90
|
+
*
|
|
91
|
+
* A themeable top-up dialog driven by the live packages/subscription-plans
|
|
92
|
+
* catalog. Call `.open()` / `.close()`, or toggle the `open` attribute.
|
|
93
|
+
* Purchases are never made client-side (the publishable key can't authorize
|
|
94
|
+
* money movement) — selecting an option dispatches a bubbling event
|
|
95
|
+
* (`crediball-select-package`, `crediball-select-plan`, or `crediball-topup`
|
|
96
|
+
* for the legacy `amounts` fallback) that the host page listens for and
|
|
97
|
+
* fulfils server-side, then calls `.close()`.
|
|
98
|
+
*/
|
|
99
|
+
declare class CrediballPaywallElement extends CrediballElement {
|
|
100
|
+
private shadow;
|
|
101
|
+
static get observedAttributes(): string[];
|
|
102
|
+
constructor();
|
|
103
|
+
open(): void;
|
|
104
|
+
close(): void;
|
|
105
|
+
protected render(): void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { CrediballBalanceElement, CrediballElement, CrediballLowCreditWarningElement, CrediballPaywallElement, CrediballTopUpButtonElement, CrediballUsageElement };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CrediballBalanceElement,
|
|
3
|
+
CrediballElement,
|
|
4
|
+
CrediballLowCreditWarningElement,
|
|
5
|
+
CrediballPaywallElement,
|
|
6
|
+
CrediballTopUpButtonElement,
|
|
7
|
+
CrediballUsageElement
|
|
8
|
+
} from "./chunk-RBGPXBOH.js";
|
|
9
|
+
export {
|
|
10
|
+
CrediballBalanceElement,
|
|
11
|
+
CrediballElement,
|
|
12
|
+
CrediballLowCreditWarningElement,
|
|
13
|
+
CrediballPaywallElement,
|
|
14
|
+
CrediballTopUpButtonElement,
|
|
15
|
+
CrediballUsageElement
|
|
16
|
+
};
|
package/dist/register.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CrediballBalanceElement,
|
|
3
|
+
CrediballLowCreditWarningElement,
|
|
4
|
+
CrediballPaywallElement,
|
|
5
|
+
CrediballTopUpButtonElement,
|
|
6
|
+
CrediballUsageElement
|
|
7
|
+
} from "./chunk-RBGPXBOH.js";
|
|
8
|
+
|
|
9
|
+
// src/register.ts
|
|
10
|
+
function define(name, ctor) {
|
|
11
|
+
if (!customElements.get(name)) customElements.define(name, ctor);
|
|
12
|
+
}
|
|
13
|
+
define("crediball-balance", CrediballBalanceElement);
|
|
14
|
+
define("crediball-usage", CrediballUsageElement);
|
|
15
|
+
define("crediball-topup-button", CrediballTopUpButtonElement);
|
|
16
|
+
define("crediball-low-credit-warning", CrediballLowCreditWarningElement);
|
|
17
|
+
define("crediball-paywall", CrediballPaywallElement);
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crediball/elements",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic web components for showing Crediball credits inside your AI app.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Filippo Rezzadore <filipporezzadore@gmail.com>",
|
|
7
|
+
"homepage": "https://crediball.app",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/filipporezzadore/Crediball.git",
|
|
11
|
+
"directory": "packages/elements"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/filipporezzadore/Crediball/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"web-components",
|
|
18
|
+
"custom-elements",
|
|
19
|
+
"credits",
|
|
20
|
+
"billing",
|
|
21
|
+
"ui",
|
|
22
|
+
"ai"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"sideEffects": [
|
|
26
|
+
"./src/register.ts",
|
|
27
|
+
"./src/iife.ts",
|
|
28
|
+
"./dist/register.js",
|
|
29
|
+
"./dist/crediball-elements.iife.js"
|
|
30
|
+
],
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"module": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./register": {
|
|
40
|
+
"types": "./dist/register.d.ts",
|
|
41
|
+
"import": "./dist/register.js"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist"
|
|
46
|
+
],
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsup",
|
|
55
|
+
"dev": "tsup --watch",
|
|
56
|
+
"prepublishOnly": "npm run build"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@crediball/core": "^0.1.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"tsup": "^8.3.5",
|
|
63
|
+
"typescript": "^5.6.3"
|
|
64
|
+
}
|
|
65
|
+
}
|