@crediball/elements 0.4.0 → 0.5.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.
@@ -1,30 +1,9 @@
1
1
  // src/base.ts
2
2
  import {
3
- getCrediballClient
3
+ getCrediballClient,
4
+ loadRemoteFont,
5
+ themeToCssVars
4
6
  } from "@crediball/core";
5
- var THEME_VARS = [
6
- ["accent", "--crediball-accent", String],
7
- ["onAccent", "--crediball-on-accent", String],
8
- ["ink", "--crediball-ink", String],
9
- ["canvas", "--crediball-canvas", String],
10
- ["hairline", "--crediball-hairline", String],
11
- ["radius", "--crediball-radius", (v) => `${v}px`],
12
- ["radiusCard", "--crediball-radius-card", (v) => `${v}px`],
13
- ["fontStack", "--crediball-font", String]
14
- ];
15
- function loadRemoteFont(url) {
16
- if (typeof document === "undefined") return;
17
- const existing = document.querySelector("link[data-crediball-font]");
18
- if (existing?.href === url) return;
19
- if (existing) existing.href = url;
20
- else {
21
- const link = document.createElement("link");
22
- link.rel = "stylesheet";
23
- link.href = url;
24
- link.setAttribute("data-crediball-font", "");
25
- document.head.appendChild(link);
26
- }
27
- }
28
7
  var _CrediballElement = class _CrediballElement extends HTMLElement {
29
8
  constructor() {
30
9
  super(...arguments);
@@ -85,9 +64,8 @@ var _CrediballElement = class _CrediballElement extends HTMLElement {
85
64
  if (this.getAttribute("apply-remote-theme") === "false") return;
86
65
  const theme = this.getSnapshot()?.packages?.ui?.theme;
87
66
  if (!theme) return;
88
- for (const [key, cssVar, fmt] of THEME_VARS) {
89
- const value = theme[key];
90
- if (value != null && value !== "") this.style.setProperty(cssVar, fmt(value));
67
+ for (const [cssVar, value] of Object.entries(themeToCssVars(theme))) {
68
+ this.style.setProperty(cssVar, value);
91
69
  }
92
70
  if (theme.fontUrl) loadRemoteFont(theme.fontUrl);
93
71
  }
@@ -110,9 +88,7 @@ _CrediballElement.CLIENT_ATTRS = /* @__PURE__ */ new Set([
110
88
  var CrediballElement = _CrediballElement;
111
89
 
112
90
  // src/format.ts
113
- function formatCredits(credits) {
114
- return new Intl.NumberFormat(void 0).format(Math.round(credits));
115
- }
91
+ import { formatCredits } from "@crediball/core";
116
92
 
117
93
  // src/balance.ts
118
94
  var CrediballBalanceElement = class extends CrediballElement {
@@ -186,6 +162,7 @@ var CrediballTopUpButtonElement = class extends CrediballElement {
186
162
  };
187
163
 
188
164
  // src/low-credit-warning.ts
165
+ import { isBalanceLow } from "@crediball/core";
189
166
  var CrediballLowCreditWarningElement = class extends CrediballElement {
190
167
  static get observedAttributes() {
191
168
  return [...CrediballElement.observedAttributes, "threshold"];
@@ -200,7 +177,7 @@ var CrediballLowCreditWarningElement = class extends CrediballElement {
200
177
  const content = snapshot?.packages?.ui?.content;
201
178
  const thresholdAttr = this.getAttribute("threshold");
202
179
  const threshold = thresholdAttr != null ? Number(thresholdAttr) : content?.lowCreditThreshold;
203
- const isLow = threshold != null && snapshot?.balance != null ? snapshot.balance < threshold : !!snapshot?.isLow;
180
+ const isLow = isBalanceLow(snapshot?.balance ?? null, threshold, !!snapshot?.isLow);
204
181
  this.style.display = isLow ? "" : "none";
205
182
  const slot = this.shadowRoot?.querySelector("slot");
206
183
  if (slot) slot.textContent = content?.lowCreditMessage ?? "Low credit balance \u2014 top up to keep going.";
@@ -208,13 +185,7 @@ var CrediballLowCreditWarningElement = class extends CrediballElement {
208
185
  };
209
186
 
210
187
  // src/paywall.ts
211
- function formatMoney(amount, currency) {
212
- try {
213
- return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
214
- } catch {
215
- return `${amount.toFixed(2)} ${currency}`;
216
- }
217
- }
188
+ import { formatMoney } from "@crediball/core";
218
189
  var CURRENCY_SYMBOLS = { EUR: "\u20AC", USD: "$", GBP: "\xA3" };
219
190
  function currencySymbolFor(currency) {
220
191
  return CURRENCY_SYMBOLS[currency.toUpperCase()] ?? currency;
@@ -485,11 +456,191 @@ function showError(el, message) {
485
456
  }
486
457
  }
487
458
 
459
+ // src/referral-card.ts
460
+ import {
461
+ fetchReferral,
462
+ captureReferral,
463
+ completeStoredReferral
464
+ } from "@crediball/core";
465
+ var HTML_ESCAPES2 = {
466
+ "&": "&amp;",
467
+ "<": "&lt;",
468
+ ">": "&gt;",
469
+ '"': "&quot;",
470
+ "'": "&#39;"
471
+ };
472
+ function escapeHtml2(s) {
473
+ return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES2[c] ?? c);
474
+ }
475
+ var STYLE2 = `
476
+ :host { font: var(--crediball-font, system-ui, -apple-system, sans-serif); color: var(--crediball-ink, #1d1d1f); }
477
+ .card {
478
+ display: flex;
479
+ flex-direction: column;
480
+ gap: 14px;
481
+ padding: 24px;
482
+ border: 1px solid var(--crediball-hairline, #e0e0e0);
483
+ border-radius: var(--crediball-radius-card, 18px);
484
+ background: var(--crediball-canvas, #ffffff);
485
+ }
486
+ .title { font-size: 16px; font-weight: 600; }
487
+ .desc { font-size: 13px; color: var(--crediball-ink-muted, #7a7a7a); margin-top: 2px; }
488
+ .row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
489
+ .link {
490
+ flex: 1 1 180px;
491
+ min-width: 0;
492
+ overflow: hidden;
493
+ text-overflow: ellipsis;
494
+ white-space: nowrap;
495
+ font-size: 14px;
496
+ border: 1px solid var(--crediball-hairline, #e0e0e0);
497
+ border-radius: var(--crediball-radius, 9999px);
498
+ padding: 8px 14px;
499
+ }
500
+ button {
501
+ appearance: none;
502
+ cursor: pointer;
503
+ font: inherit;
504
+ font-size: 14px;
505
+ padding: 8px 16px;
506
+ border-radius: var(--crediball-radius, 9999px);
507
+ white-space: nowrap;
508
+ }
509
+ button.copy { border: 1px solid var(--crediball-hairline, #e0e0e0); background: transparent; color: var(--crediball-ink, #1d1d1f); }
510
+ button.share { border: none; background: var(--crediball-accent, #0066cc); color: var(--crediball-on-accent, #ffffff); }
511
+ .stats { display: flex; gap: 16px; font-size: 13px; color: var(--crediball-ink-muted, #7a7a7a); }
512
+ .stats strong { color: var(--crediball-ink, #1d1d1f); font-weight: 600; }
513
+ `;
514
+ var CrediballReferralCardElement = class extends CrediballElement {
515
+ constructor() {
516
+ super();
517
+ this.data = null;
518
+ this.copied = false;
519
+ this.copiedTimer = null;
520
+ this.shadow = this.attachShadow({ mode: "open" });
521
+ }
522
+ static get observedAttributes() {
523
+ return [
524
+ ...CrediballElement.observedAttributes,
525
+ "title",
526
+ "description",
527
+ "copy-label",
528
+ "share-label",
529
+ "hide-stats"
530
+ ];
531
+ }
532
+ connectedCallback() {
533
+ super.connectedCallback();
534
+ void this.loadReferral();
535
+ }
536
+ disconnectedCallback() {
537
+ super.disconnectedCallback();
538
+ if (this.copiedTimer) clearTimeout(this.copiedTimer);
539
+ }
540
+ attributeChangedCallback(name) {
541
+ super.attributeChangedCallback(name);
542
+ if (!this.isConnected) return;
543
+ if (name === "publishable-key" || name === "user-id" || name === "api-url") {
544
+ void this.loadReferral();
545
+ }
546
+ }
547
+ /** Auto-capture ?ref=, auto-complete once a user is known, then fetch the card data. */
548
+ async loadReferral() {
549
+ const publishableKey = this.getAttribute("publishable-key");
550
+ const userId = this.getAttribute("user-id");
551
+ const apiUrl = this.getAttribute("api-url") ?? void 0;
552
+ if (!publishableKey) return;
553
+ if (this.getAttribute("capture-referrals") !== "false") {
554
+ captureReferral({ publishableKey, apiUrl });
555
+ if (userId) {
556
+ void completeStoredReferral({ publishableKey, userId, apiUrl }).catch(() => {
557
+ });
558
+ }
559
+ }
560
+ if (!userId) {
561
+ this.data = null;
562
+ this.render();
563
+ return;
564
+ }
565
+ try {
566
+ this.data = await fetchReferral({ publishableKey, userId, apiUrl });
567
+ } catch {
568
+ this.data = null;
569
+ }
570
+ this.render();
571
+ }
572
+ render() {
573
+ const d = this.data;
574
+ if (!d || !d.enabled || !d.code) {
575
+ this.shadow.innerHTML = "";
576
+ return;
577
+ }
578
+ const title = this.getAttribute("title") ?? "Invite friends";
579
+ const copyLabel = this.getAttribute("copy-label") ?? "Copy";
580
+ const shareLabel = this.getAttribute("share-label") ?? "Share";
581
+ const hideStats = this.getAttribute("hide-stats") != null;
582
+ const reward = d.rewardCredits ?? 0;
583
+ const referredReward = d.referredRewardCredits ?? 0;
584
+ const description = this.getAttribute("description") ?? (reward > 0 ? referredReward > 0 ? `Earn ${formatCredits(reward)} credits for every friend who joins \u2014 they get ${formatCredits(referredReward)} credits too.` : `Earn ${formatCredits(reward)} credits for every friend who joins.` : null);
585
+ const shown = d.link ?? d.code;
586
+ const rewards = d.rewards ?? { pending: 0, converted: 0, creditsEarned: 0 };
587
+ const canShare = typeof navigator !== "undefined" && typeof navigator.share === "function";
588
+ this.shadow.innerHTML = `
589
+ <style>${STYLE2}</style>
590
+ <div class="card" part="card">
591
+ <div>
592
+ <div class="title">${escapeHtml2(title)}</div>
593
+ ${description ? `<div class="desc">${escapeHtml2(description)}</div>` : ""}
594
+ </div>
595
+ <div class="row">
596
+ <div class="link" part="link" title="${escapeHtml2(shown)}">${escapeHtml2(shown)}</div>
597
+ <button class="copy" part="copy-button" type="button">${escapeHtml2(this.copied ? "Copied!" : copyLabel)}</button>
598
+ ${canShare ? `<button class="share" part="share-button" type="button">${escapeHtml2(shareLabel)}</button>` : ""}
599
+ </div>
600
+ ${hideStats ? "" : `<div class="stats">
601
+ <span><strong>${d.clicks ?? 0}</strong> clicks</span>
602
+ <span><strong>${rewards.converted}</strong> joined</span>
603
+ ${rewards.creditsEarned > 0 ? `<span><strong>${formatCredits(rewards.creditsEarned)}</strong> credits earned</span>` : ""}
604
+ </div>`}
605
+ </div>
606
+ `;
607
+ this.shadow.querySelector("button.copy")?.addEventListener("click", () => void this.copy());
608
+ this.shadow.querySelector("button.share")?.addEventListener("click", () => void this.share());
609
+ }
610
+ linkOrCode() {
611
+ return this.data?.link ?? this.data?.code ?? null;
612
+ }
613
+ async copy() {
614
+ const text = this.linkOrCode();
615
+ if (!text || typeof navigator === "undefined" || !navigator.clipboard) return;
616
+ await navigator.clipboard.writeText(text);
617
+ this.copied = true;
618
+ this.render();
619
+ if (this.copiedTimer) clearTimeout(this.copiedTimer);
620
+ this.copiedTimer = setTimeout(() => {
621
+ this.copied = false;
622
+ this.render();
623
+ }, 2e3);
624
+ }
625
+ async share() {
626
+ const url = this.data?.link ?? void 0;
627
+ if (url && typeof navigator !== "undefined" && typeof navigator.share === "function") {
628
+ try {
629
+ await navigator.share({ url });
630
+ return;
631
+ } catch {
632
+ }
633
+ }
634
+ await this.copy();
635
+ }
636
+ };
637
+
488
638
  export {
489
639
  CrediballElement,
490
640
  CrediballBalanceElement,
491
641
  CrediballUsageElement,
492
642
  CrediballTopUpButtonElement,
493
643
  CrediballLowCreditWarningElement,
494
- CrediballPaywallElement
644
+ CrediballPaywallElement,
645
+ CrediballReferralCardElement
495
646
  };
@@ -25,6 +25,7 @@ var Crediball = (() => {
25
25
  CrediballElement: () => CrediballElement,
26
26
  CrediballLowCreditWarningElement: () => CrediballLowCreditWarningElement,
27
27
  CrediballPaywallElement: () => CrediballPaywallElement,
28
+ CrediballReferralCardElement: () => CrediballReferralCardElement,
28
29
  CrediballTopUpButtonElement: () => CrediballTopUpButtonElement,
29
30
  CrediballUsageElement: () => CrediballUsageElement
30
31
  });
@@ -68,8 +69,21 @@ var Crediball = (() => {
68
69
  return () => window.removeEventListener(BALANCE_CHANGED_EVENT, listener);
69
70
  }
70
71
 
71
- // ../core/dist/client.js
72
+ // ../core/dist/http.js
72
73
  var DEFAULT_API_URL = "https://www.crediball.ai/api";
74
+ async function errorMessage(res, fallback) {
75
+ const body = await res.json().catch(() => null);
76
+ return body && typeof body === "object" && "error" in body && String(body.error) || fallback;
77
+ }
78
+ async function fetchJson(url, init, failureLabel = `Request to ${url}`) {
79
+ const res = await fetch(url, init);
80
+ if (!res.ok) {
81
+ throw new Error(await errorMessage(res, `${failureLabel} failed with ${res.status}`));
82
+ }
83
+ return res.json();
84
+ }
85
+
86
+ // ../core/dist/client.js
73
87
  var DEFAULT_POLL_MS = 3e4;
74
88
  var SSE_HEALTHY_POLL_MULTIPLIER = 2;
75
89
  var MAX_SSE_FAILURES = 2;
@@ -85,15 +99,6 @@ var Crediball = (() => {
85
99
  error: null
86
100
  };
87
101
  }
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
102
  function computeIsLow(balance, costs, explicitThreshold) {
98
103
  if (balance == null)
99
104
  return false;
@@ -134,10 +139,10 @@ var Crediball = (() => {
134
139
  const qs = `userId=${encodeURIComponent(userId)}`;
135
140
  try {
136
141
  const [balanceRes, usageRes, costsRes, packagesRes] = await Promise.all([
137
- fetchJson(`${this.apiUrl}/public/balance?${qs}`, headers),
138
- fetchJson(`${this.apiUrl}/public/usage?${qs}`, headers),
139
- fetchJson(`${this.apiUrl}/public/costs`, headers),
140
- fetchJson(`${this.apiUrl}/public/packages?${qs}`, headers)
142
+ fetchJson(`${this.apiUrl}/public/balance?${qs}`, { headers }),
143
+ fetchJson(`${this.apiUrl}/public/usage?${qs}`, { headers }),
144
+ fetchJson(`${this.apiUrl}/public/costs`, { headers }),
145
+ fetchJson(`${this.apiUrl}/public/packages?${qs}`, { headers })
141
146
  ]);
142
147
  const costs = {};
143
148
  for (const e of costsRes.events)
@@ -188,22 +193,16 @@ var Crediball = (() => {
188
193
  this.apiUrl = (config.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
189
194
  this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_MS;
190
195
  }
191
- async postCheckout(path, input) {
196
+ postCheckout(path, input) {
192
197
  const { publishableKey, userId } = this.config;
193
- const res = await fetch(`${this.apiUrl}${path}`, {
198
+ return fetchJson(`${this.apiUrl}${path}`, {
194
199
  method: "POST",
195
200
  headers: {
196
201
  "Content-Type": "application/json",
197
202
  Authorization: `Bearer ${publishableKey}`
198
203
  },
199
204
  body: JSON.stringify({ userId, ...input })
200
- });
201
- if (!res.ok) {
202
- const body = await res.json().catch(() => null);
203
- const message = body && typeof body === "object" && "error" in body && String(body.error) || `Checkout request failed with ${res.status}`;
204
- throw new Error(message);
205
- }
206
- return res.json();
205
+ }, "Checkout request");
207
206
  }
208
207
  pause() {
209
208
  this.stopPolling();
@@ -303,8 +302,86 @@ var Crediball = (() => {
303
302
  return client;
304
303
  }
305
304
 
306
- // src/base.ts
307
- var THEME_VARS = [
305
+ // ../core/dist/referral.js
306
+ var storageKey = (publishableKey) => `crediball_ref_${publishableKey}`;
307
+ function apiBase(config) {
308
+ return (config.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
309
+ }
310
+ function authHeaders(config) {
311
+ return { Authorization: `Bearer ${config.publishableKey}` };
312
+ }
313
+ function getStoredReferralCode(publishableKey) {
314
+ try {
315
+ return window.localStorage.getItem(storageKey(publishableKey));
316
+ } catch {
317
+ return null;
318
+ }
319
+ }
320
+ function clearStoredReferralCode(publishableKey) {
321
+ try {
322
+ window.localStorage.removeItem(storageKey(publishableKey));
323
+ } catch {
324
+ }
325
+ }
326
+ function captureReferral(config, code) {
327
+ let captured = code?.trim();
328
+ if (!captured && typeof window !== "undefined") {
329
+ captured = new URLSearchParams(window.location.search).get("ref")?.trim() ?? void 0;
330
+ }
331
+ if (!captured)
332
+ return;
333
+ captured = captured.toUpperCase();
334
+ const already = getStoredReferralCode(config.publishableKey);
335
+ try {
336
+ window.localStorage.setItem(storageKey(config.publishableKey), captured);
337
+ } catch {
338
+ return;
339
+ }
340
+ if (already === captured)
341
+ return;
342
+ void fetchJson(`${apiBase(config)}/public/referral/click`, {
343
+ method: "POST",
344
+ headers: { "Content-Type": "application/json", ...authHeaders(config) },
345
+ body: JSON.stringify({ code: captured })
346
+ }, "Referral click").catch(() => {
347
+ });
348
+ }
349
+ async function completeStoredReferral(config) {
350
+ const code = getStoredReferralCode(config.publishableKey);
351
+ if (!code || !config.userId)
352
+ return;
353
+ await fetchJson(`${apiBase(config)}/public/referral/complete`, {
354
+ method: "POST",
355
+ headers: { "Content-Type": "application/json", ...authHeaders(config) },
356
+ body: JSON.stringify({ code, userId: config.userId })
357
+ }, "Referral completion");
358
+ clearStoredReferralCode(config.publishableKey);
359
+ }
360
+ async function fetchReferral(config) {
361
+ const data = await fetchJson(`${apiBase(config)}/public/referral?userId=${encodeURIComponent(config.userId)}`, { headers: authHeaders(config) }, "Referral fetch");
362
+ if (data.enabled && data.code && !data.link && typeof window !== "undefined") {
363
+ data.link = `${window.location.origin}/?ref=${data.code}`;
364
+ }
365
+ return data;
366
+ }
367
+
368
+ // ../core/dist/format.js
369
+ function formatCredits(credits) {
370
+ return new Intl.NumberFormat(void 0).format(Math.round(credits));
371
+ }
372
+ function formatMoney(amount, currency) {
373
+ try {
374
+ return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
375
+ } catch {
376
+ return `${amount.toFixed(2)} ${currency}`;
377
+ }
378
+ }
379
+ function isBalanceLow(balance, threshold, fallback) {
380
+ return threshold != null && balance != null ? balance < threshold : fallback;
381
+ }
382
+
383
+ // ../core/dist/theme.js
384
+ var THEME_CSS_VAR_ENTRIES = [
308
385
  ["accent", "--crediball-accent", String],
309
386
  ["onAccent", "--crediball-on-accent", String],
310
387
  ["ink", "--crediball-ink", String],
@@ -314,11 +391,25 @@ var Crediball = (() => {
314
391
  ["radiusCard", "--crediball-radius-card", (v) => `${v}px`],
315
392
  ["fontStack", "--crediball-font", String]
316
393
  ];
394
+ function themeToCssVars(theme) {
395
+ const vars = {};
396
+ if (!theme)
397
+ return vars;
398
+ for (const [key, cssVar, fmt] of THEME_CSS_VAR_ENTRIES) {
399
+ const value = theme[key];
400
+ if (value != null && value !== "")
401
+ vars[cssVar] = fmt(value);
402
+ }
403
+ return vars;
404
+ }
317
405
  function loadRemoteFont(url) {
318
- if (typeof document === "undefined") return;
406
+ if (typeof document === "undefined")
407
+ return;
319
408
  const existing = document.querySelector("link[data-crediball-font]");
320
- if (existing?.href === url) return;
321
- if (existing) existing.href = url;
409
+ if (existing?.href === url)
410
+ return;
411
+ if (existing)
412
+ existing.href = url;
322
413
  else {
323
414
  const link = document.createElement("link");
324
415
  link.rel = "stylesheet";
@@ -327,6 +418,8 @@ var Crediball = (() => {
327
418
  document.head.appendChild(link);
328
419
  }
329
420
  }
421
+
422
+ // src/base.ts
330
423
  var _CrediballElement = class _CrediballElement extends HTMLElement {
331
424
  constructor() {
332
425
  super(...arguments);
@@ -387,9 +480,8 @@ var Crediball = (() => {
387
480
  if (this.getAttribute("apply-remote-theme") === "false") return;
388
481
  const theme = this.getSnapshot()?.packages?.ui?.theme;
389
482
  if (!theme) return;
390
- for (const [key, cssVar, fmt] of THEME_VARS) {
391
- const value = theme[key];
392
- if (value != null && value !== "") this.style.setProperty(cssVar, fmt(value));
483
+ for (const [cssVar, value] of Object.entries(themeToCssVars(theme))) {
484
+ this.style.setProperty(cssVar, value);
393
485
  }
394
486
  if (theme.fontUrl) loadRemoteFont(theme.fontUrl);
395
487
  }
@@ -411,11 +503,6 @@ var Crediball = (() => {
411
503
  ]);
412
504
  var CrediballElement = _CrediballElement;
413
505
 
414
- // src/format.ts
415
- function formatCredits(credits) {
416
- return new Intl.NumberFormat(void 0).format(Math.round(credits));
417
- }
418
-
419
506
  // src/balance.ts
420
507
  var CrediballBalanceElement = class extends CrediballElement {
421
508
  constructor() {
@@ -502,7 +589,7 @@ var Crediball = (() => {
502
589
  const content = snapshot?.packages?.ui?.content;
503
590
  const thresholdAttr = this.getAttribute("threshold");
504
591
  const threshold = thresholdAttr != null ? Number(thresholdAttr) : content?.lowCreditThreshold;
505
- const isLow = threshold != null && snapshot?.balance != null ? snapshot.balance < threshold : !!snapshot?.isLow;
592
+ const isLow = isBalanceLow(snapshot?.balance ?? null, threshold, !!snapshot?.isLow);
506
593
  this.style.display = isLow ? "" : "none";
507
594
  const slot = this.shadowRoot?.querySelector("slot");
508
595
  if (slot) slot.textContent = content?.lowCreditMessage ?? "Low credit balance \u2014 top up to keep going.";
@@ -510,13 +597,6 @@ var Crediball = (() => {
510
597
  };
511
598
 
512
599
  // src/paywall.ts
513
- function formatMoney(amount, currency) {
514
- try {
515
- return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
516
- } catch {
517
- return `${amount.toFixed(2)} ${currency}`;
518
- }
519
- }
520
600
  var CURRENCY_SYMBOLS = { EUR: "\u20AC", USD: "$", GBP: "\xA3" };
521
601
  function currencySymbolFor(currency) {
522
602
  return CURRENCY_SYMBOLS[currency.toUpperCase()] ?? currency;
@@ -787,6 +867,180 @@ var Crediball = (() => {
787
867
  }
788
868
  }
789
869
 
870
+ // src/referral-card.ts
871
+ var HTML_ESCAPES2 = {
872
+ "&": "&amp;",
873
+ "<": "&lt;",
874
+ ">": "&gt;",
875
+ '"': "&quot;",
876
+ "'": "&#39;"
877
+ };
878
+ function escapeHtml2(s) {
879
+ return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES2[c] ?? c);
880
+ }
881
+ var STYLE2 = `
882
+ :host { font: var(--crediball-font, system-ui, -apple-system, sans-serif); color: var(--crediball-ink, #1d1d1f); }
883
+ .card {
884
+ display: flex;
885
+ flex-direction: column;
886
+ gap: 14px;
887
+ padding: 24px;
888
+ border: 1px solid var(--crediball-hairline, #e0e0e0);
889
+ border-radius: var(--crediball-radius-card, 18px);
890
+ background: var(--crediball-canvas, #ffffff);
891
+ }
892
+ .title { font-size: 16px; font-weight: 600; }
893
+ .desc { font-size: 13px; color: var(--crediball-ink-muted, #7a7a7a); margin-top: 2px; }
894
+ .row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
895
+ .link {
896
+ flex: 1 1 180px;
897
+ min-width: 0;
898
+ overflow: hidden;
899
+ text-overflow: ellipsis;
900
+ white-space: nowrap;
901
+ font-size: 14px;
902
+ border: 1px solid var(--crediball-hairline, #e0e0e0);
903
+ border-radius: var(--crediball-radius, 9999px);
904
+ padding: 8px 14px;
905
+ }
906
+ button {
907
+ appearance: none;
908
+ cursor: pointer;
909
+ font: inherit;
910
+ font-size: 14px;
911
+ padding: 8px 16px;
912
+ border-radius: var(--crediball-radius, 9999px);
913
+ white-space: nowrap;
914
+ }
915
+ button.copy { border: 1px solid var(--crediball-hairline, #e0e0e0); background: transparent; color: var(--crediball-ink, #1d1d1f); }
916
+ button.share { border: none; background: var(--crediball-accent, #0066cc); color: var(--crediball-on-accent, #ffffff); }
917
+ .stats { display: flex; gap: 16px; font-size: 13px; color: var(--crediball-ink-muted, #7a7a7a); }
918
+ .stats strong { color: var(--crediball-ink, #1d1d1f); font-weight: 600; }
919
+ `;
920
+ var CrediballReferralCardElement = class extends CrediballElement {
921
+ constructor() {
922
+ super();
923
+ this.data = null;
924
+ this.copied = false;
925
+ this.copiedTimer = null;
926
+ this.shadow = this.attachShadow({ mode: "open" });
927
+ }
928
+ static get observedAttributes() {
929
+ return [
930
+ ...CrediballElement.observedAttributes,
931
+ "title",
932
+ "description",
933
+ "copy-label",
934
+ "share-label",
935
+ "hide-stats"
936
+ ];
937
+ }
938
+ connectedCallback() {
939
+ super.connectedCallback();
940
+ void this.loadReferral();
941
+ }
942
+ disconnectedCallback() {
943
+ super.disconnectedCallback();
944
+ if (this.copiedTimer) clearTimeout(this.copiedTimer);
945
+ }
946
+ attributeChangedCallback(name) {
947
+ super.attributeChangedCallback(name);
948
+ if (!this.isConnected) return;
949
+ if (name === "publishable-key" || name === "user-id" || name === "api-url") {
950
+ void this.loadReferral();
951
+ }
952
+ }
953
+ /** Auto-capture ?ref=, auto-complete once a user is known, then fetch the card data. */
954
+ async loadReferral() {
955
+ const publishableKey = this.getAttribute("publishable-key");
956
+ const userId = this.getAttribute("user-id");
957
+ const apiUrl = this.getAttribute("api-url") ?? void 0;
958
+ if (!publishableKey) return;
959
+ if (this.getAttribute("capture-referrals") !== "false") {
960
+ captureReferral({ publishableKey, apiUrl });
961
+ if (userId) {
962
+ void completeStoredReferral({ publishableKey, userId, apiUrl }).catch(() => {
963
+ });
964
+ }
965
+ }
966
+ if (!userId) {
967
+ this.data = null;
968
+ this.render();
969
+ return;
970
+ }
971
+ try {
972
+ this.data = await fetchReferral({ publishableKey, userId, apiUrl });
973
+ } catch {
974
+ this.data = null;
975
+ }
976
+ this.render();
977
+ }
978
+ render() {
979
+ const d = this.data;
980
+ if (!d || !d.enabled || !d.code) {
981
+ this.shadow.innerHTML = "";
982
+ return;
983
+ }
984
+ const title = this.getAttribute("title") ?? "Invite friends";
985
+ const copyLabel = this.getAttribute("copy-label") ?? "Copy";
986
+ const shareLabel = this.getAttribute("share-label") ?? "Share";
987
+ const hideStats = this.getAttribute("hide-stats") != null;
988
+ const reward = d.rewardCredits ?? 0;
989
+ const referredReward = d.referredRewardCredits ?? 0;
990
+ const description = this.getAttribute("description") ?? (reward > 0 ? referredReward > 0 ? `Earn ${formatCredits(reward)} credits for every friend who joins \u2014 they get ${formatCredits(referredReward)} credits too.` : `Earn ${formatCredits(reward)} credits for every friend who joins.` : null);
991
+ const shown = d.link ?? d.code;
992
+ const rewards = d.rewards ?? { pending: 0, converted: 0, creditsEarned: 0 };
993
+ const canShare = typeof navigator !== "undefined" && typeof navigator.share === "function";
994
+ this.shadow.innerHTML = `
995
+ <style>${STYLE2}</style>
996
+ <div class="card" part="card">
997
+ <div>
998
+ <div class="title">${escapeHtml2(title)}</div>
999
+ ${description ? `<div class="desc">${escapeHtml2(description)}</div>` : ""}
1000
+ </div>
1001
+ <div class="row">
1002
+ <div class="link" part="link" title="${escapeHtml2(shown)}">${escapeHtml2(shown)}</div>
1003
+ <button class="copy" part="copy-button" type="button">${escapeHtml2(this.copied ? "Copied!" : copyLabel)}</button>
1004
+ ${canShare ? `<button class="share" part="share-button" type="button">${escapeHtml2(shareLabel)}</button>` : ""}
1005
+ </div>
1006
+ ${hideStats ? "" : `<div class="stats">
1007
+ <span><strong>${d.clicks ?? 0}</strong> clicks</span>
1008
+ <span><strong>${rewards.converted}</strong> joined</span>
1009
+ ${rewards.creditsEarned > 0 ? `<span><strong>${formatCredits(rewards.creditsEarned)}</strong> credits earned</span>` : ""}
1010
+ </div>`}
1011
+ </div>
1012
+ `;
1013
+ this.shadow.querySelector("button.copy")?.addEventListener("click", () => void this.copy());
1014
+ this.shadow.querySelector("button.share")?.addEventListener("click", () => void this.share());
1015
+ }
1016
+ linkOrCode() {
1017
+ return this.data?.link ?? this.data?.code ?? null;
1018
+ }
1019
+ async copy() {
1020
+ const text = this.linkOrCode();
1021
+ if (!text || typeof navigator === "undefined" || !navigator.clipboard) return;
1022
+ await navigator.clipboard.writeText(text);
1023
+ this.copied = true;
1024
+ this.render();
1025
+ if (this.copiedTimer) clearTimeout(this.copiedTimer);
1026
+ this.copiedTimer = setTimeout(() => {
1027
+ this.copied = false;
1028
+ this.render();
1029
+ }, 2e3);
1030
+ }
1031
+ async share() {
1032
+ const url = this.data?.link ?? void 0;
1033
+ if (url && typeof navigator !== "undefined" && typeof navigator.share === "function") {
1034
+ try {
1035
+ await navigator.share({ url });
1036
+ return;
1037
+ } catch {
1038
+ }
1039
+ }
1040
+ await this.copy();
1041
+ }
1042
+ };
1043
+
790
1044
  // src/register.ts
791
1045
  function define(name, ctor) {
792
1046
  if (!customElements.get(name)) customElements.define(name, ctor);
@@ -796,5 +1050,6 @@ var Crediball = (() => {
796
1050
  define("crediball-topup-button", CrediballTopUpButtonElement);
797
1051
  define("crediball-low-credit-warning", CrediballLowCreditWarningElement);
798
1052
  define("crediball-paywall", CrediballPaywallElement);
1053
+ define("crediball-referral-card", CrediballReferralCardElement);
799
1054
  return __toCommonJS(iife_exports);
800
1055
  })();
package/dist/index.d.ts CHANGED
@@ -125,4 +125,39 @@ declare class CrediballPaywallElement extends CrediballElement {
125
125
  private redirect;
126
126
  }
127
127
 
128
- export { CrediballBalanceElement, CrediballElement, CrediballLowCreditWarningElement, CrediballPaywallElement, CrediballTopUpButtonElement, CrediballUsageElement };
128
+ /**
129
+ * <crediball-referral-card publishable-key="cb_pub_..." user-id="u123"></crediball-referral-card>
130
+ *
131
+ * Drop-in invite card — the web-component sibling of @crediball/react's
132
+ * <ReferralCard/>, wrapping the same @crediball/core referral functions:
133
+ * - lazily creates + fetches the user's referral code/link/stats,
134
+ * - AUTO-CAPTURES a `?ref=CODE` visit into localStorage and, once a user-id is
135
+ * present, attaches it as a pending referral (set `capture-referrals="false"`
136
+ * to opt out and drive capture/completion yourself),
137
+ * - renders nothing while loading or when the referral program is disabled in
138
+ * the dashboard.
139
+ *
140
+ * Rewards are only ever granted server-side, when the dashboard-configured
141
+ * conversion condition is observed on a secret-key call — a browser can't
142
+ * trigger a payout. Optional attributes: title, description, copy-label,
143
+ * share-label, hide-stats.
144
+ */
145
+ declare class CrediballReferralCardElement extends CrediballElement {
146
+ private shadow;
147
+ private data;
148
+ private copied;
149
+ private copiedTimer;
150
+ static get observedAttributes(): string[];
151
+ constructor();
152
+ connectedCallback(): void;
153
+ disconnectedCallback(): void;
154
+ attributeChangedCallback(name: string): void;
155
+ /** Auto-capture ?ref=, auto-complete once a user is known, then fetch the card data. */
156
+ private loadReferral;
157
+ protected render(): void;
158
+ private linkOrCode;
159
+ private copy;
160
+ private share;
161
+ }
162
+
163
+ export { CrediballBalanceElement, CrediballElement, CrediballLowCreditWarningElement, CrediballPaywallElement, CrediballReferralCardElement, CrediballTopUpButtonElement, CrediballUsageElement };
package/dist/index.js CHANGED
@@ -3,14 +3,16 @@ import {
3
3
  CrediballElement,
4
4
  CrediballLowCreditWarningElement,
5
5
  CrediballPaywallElement,
6
+ CrediballReferralCardElement,
6
7
  CrediballTopUpButtonElement,
7
8
  CrediballUsageElement
8
- } from "./chunk-KXVW7PIU.js";
9
+ } from "./chunk-6CNCZGCF.js";
9
10
  export {
10
11
  CrediballBalanceElement,
11
12
  CrediballElement,
12
13
  CrediballLowCreditWarningElement,
13
14
  CrediballPaywallElement,
15
+ CrediballReferralCardElement,
14
16
  CrediballTopUpButtonElement,
15
17
  CrediballUsageElement
16
18
  };
package/dist/register.js CHANGED
@@ -2,9 +2,10 @@ import {
2
2
  CrediballBalanceElement,
3
3
  CrediballLowCreditWarningElement,
4
4
  CrediballPaywallElement,
5
+ CrediballReferralCardElement,
5
6
  CrediballTopUpButtonElement,
6
7
  CrediballUsageElement
7
- } from "./chunk-KXVW7PIU.js";
8
+ } from "./chunk-6CNCZGCF.js";
8
9
 
9
10
  // src/register.ts
10
11
  function define(name, ctor) {
@@ -15,3 +16,4 @@ define("crediball-usage", CrediballUsageElement);
15
16
  define("crediball-topup-button", CrediballTopUpButtonElement);
16
17
  define("crediball-low-credit-warning", CrediballLowCreditWarningElement);
17
18
  define("crediball-paywall", CrediballPaywallElement);
19
+ define("crediball-referral-card", CrediballReferralCardElement);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crediball/elements",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Framework-agnostic web components for showing Crediball credits inside your AI app.",
5
5
  "license": "MIT",
6
6
  "author": "Filippo Rezzadore <filipporezzadore@gmail.com>",
@@ -56,7 +56,7 @@
56
56
  "prepublishOnly": "npm run build"
57
57
  },
58
58
  "dependencies": {
59
- "@crediball/core": "^0.3.0"
59
+ "@crediball/core": "^0.4.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "tsup": "^8.3.5",