@crediball/elements 0.4.1 → 0.6.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.
@@ -185,7 +185,7 @@ var CrediballLowCreditWarningElement = class extends CrediballElement {
185
185
  };
186
186
 
187
187
  // src/paywall.ts
188
- import { formatMoney } from "@crediball/core";
188
+ import { formatMoney, fetchReferral } from "@crediball/core";
189
189
  var CURRENCY_SYMBOLS = { EUR: "\u20AC", USD: "$", GBP: "\xA3" };
190
190
  function currencySymbolFor(currency) {
191
191
  return CURRENCY_SYMBOLS[currency.toUpperCase()] ?? currency;
@@ -283,15 +283,59 @@ var STYLE = `
283
283
  padding: 11px 22px;
284
284
  border-radius: var(--crediball-radius, 9999px);
285
285
  }
286
+ .referral { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--crediball-hairline, #e0e0e0); }
287
+ .referral .eyebrow { margin-bottom: 8px; }
288
+ .referral-row { display: flex; gap: 8px; align-items: center; }
289
+ .referral-row .link {
290
+ flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
291
+ font-size: 13px; color: var(--crediball-ink, #1d1d1f);
292
+ border: 1px solid var(--crediball-hairline, #e0e0e0);
293
+ border-radius: var(--crediball-radius, 9999px); padding: 8px 14px;
294
+ }
295
+ button.referral-copy {
296
+ flex: none; appearance: none; cursor: pointer; background: transparent;
297
+ color: var(--crediball-ink, #1d1d1f); border: 1px solid var(--crediball-hairline, #e0e0e0);
298
+ font: inherit; font-size: 14px; padding: 8px 16px; border-radius: var(--crediball-radius, 9999px);
299
+ white-space: nowrap;
300
+ }
286
301
  `;
287
302
  var CrediballPaywallElement = class extends CrediballElement {
288
- static get observedAttributes() {
289
- return [...CrediballElement.observedAttributes, "open", "title", "description", "amounts"];
290
- }
291
303
  constructor() {
292
304
  super();
305
+ this.referral = null;
306
+ this.referralCopied = false;
293
307
  this.shadow = this.attachShadow({ mode: "open" });
294
308
  }
309
+ static get observedAttributes() {
310
+ return [...CrediballElement.observedAttributes, "open", "title", "description", "amounts"];
311
+ }
312
+ connectedCallback() {
313
+ super.connectedCallback();
314
+ void this.loadReferral();
315
+ }
316
+ attributeChangedCallback(name) {
317
+ super.attributeChangedCallback(name);
318
+ if (this.isConnected && (name === "publishable-key" || name === "user-id" || name === "api-url")) {
319
+ void this.loadReferral();
320
+ }
321
+ }
322
+ /** Fetch the user's invite link for the "Refer a friend" row (best-effort). */
323
+ async loadReferral() {
324
+ const publishableKey = this.getAttribute("publishable-key");
325
+ const userId = this.getAttribute("user-id");
326
+ const apiUrl = this.getAttribute("api-url") ?? void 0;
327
+ if (!publishableKey || !userId) {
328
+ this.referral = null;
329
+ return;
330
+ }
331
+ try {
332
+ const data = await fetchReferral({ publishableKey, userId, apiUrl });
333
+ this.referral = data.enabled ? data : null;
334
+ } catch {
335
+ this.referral = null;
336
+ }
337
+ if (this.hasAttribute("open")) this.render();
338
+ }
295
339
  open() {
296
340
  this.setAttribute("open", "");
297
341
  }
@@ -357,6 +401,13 @@ var CrediballPaywallElement = class extends CrediballElement {
357
401
  <button class="custom-add" type="button">${escapeHtml(addButtonText)}</button>
358
402
  </div>` : ""}
359
403
  <p class="error" hidden></p>
404
+ ${this.referral?.link ? `<div class="referral">
405
+ <div class="eyebrow">${this.referral.rewardCredits && this.referral.rewardCredits > 0 ? `Refer a friend \xB7 earn ${formatCredits(this.referral.rewardCredits)} credits` : "Refer a friend"}</div>
406
+ <div class="referral-row">
407
+ <div class="link" title="${escapeHtml(this.referral.link)}">${escapeHtml(this.referral.link)}</div>
408
+ <button class="referral-copy" type="button">${this.referralCopied ? "Copied!" : "Copy"}</button>
409
+ </div>
410
+ </div>` : ""}
360
411
  <button class="close" part="close" type="button">Not now</button>
361
412
  </div>
362
413
  </div>
@@ -365,6 +416,7 @@ var CrediballPaywallElement = class extends CrediballElement {
365
416
  if (e.target === e.currentTarget) this.close();
366
417
  });
367
418
  this.shadow.querySelector(".close")?.addEventListener("click", () => this.close());
419
+ this.shadow.querySelector(".referral-copy")?.addEventListener("click", () => void this.copyReferral());
368
420
  this.shadow.querySelectorAll("button.option").forEach((el) => {
369
421
  const btn = el;
370
422
  btn.addEventListener("click", () => {
@@ -414,6 +466,18 @@ var CrediballPaywallElement = class extends CrediballElement {
414
466
  });
415
467
  }
416
468
  }
469
+ /** Copy the invite link to the clipboard, with a 2s "Copied!" confirmation. */
470
+ async copyReferral() {
471
+ const link = this.referral?.link;
472
+ if (!link || typeof navigator === "undefined" || !navigator.clipboard) return;
473
+ await navigator.clipboard.writeText(link);
474
+ this.referralCopied = true;
475
+ this.render();
476
+ setTimeout(() => {
477
+ this.referralCopied = false;
478
+ if (this.hasAttribute("open")) this.render();
479
+ }, 2e3);
480
+ }
417
481
  /** Dispatch a cancelable option event; returns true when the host did NOT preventDefault. */
418
482
  emit(name, detail) {
419
483
  const ev = new CustomEvent(name, { detail, bubbles: true, composed: true, cancelable: true });
@@ -456,11 +520,191 @@ function showError(el, message) {
456
520
  }
457
521
  }
458
522
 
523
+ // src/referral-card.ts
524
+ import {
525
+ fetchReferral as fetchReferral2,
526
+ captureReferral,
527
+ completeStoredReferral
528
+ } from "@crediball/core";
529
+ var HTML_ESCAPES2 = {
530
+ "&": "&amp;",
531
+ "<": "&lt;",
532
+ ">": "&gt;",
533
+ '"': "&quot;",
534
+ "'": "&#39;"
535
+ };
536
+ function escapeHtml2(s) {
537
+ return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES2[c] ?? c);
538
+ }
539
+ var STYLE2 = `
540
+ :host { font: var(--crediball-font, system-ui, -apple-system, sans-serif); color: var(--crediball-ink, #1d1d1f); }
541
+ .card {
542
+ display: flex;
543
+ flex-direction: column;
544
+ gap: 14px;
545
+ padding: 24px;
546
+ border: 1px solid var(--crediball-hairline, #e0e0e0);
547
+ border-radius: var(--crediball-radius-card, 18px);
548
+ background: var(--crediball-canvas, #ffffff);
549
+ }
550
+ .title { font-size: 16px; font-weight: 600; }
551
+ .desc { font-size: 13px; color: var(--crediball-ink-muted, #7a7a7a); margin-top: 2px; }
552
+ .row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
553
+ .link {
554
+ flex: 1 1 180px;
555
+ min-width: 0;
556
+ overflow: hidden;
557
+ text-overflow: ellipsis;
558
+ white-space: nowrap;
559
+ font-size: 14px;
560
+ border: 1px solid var(--crediball-hairline, #e0e0e0);
561
+ border-radius: var(--crediball-radius, 9999px);
562
+ padding: 8px 14px;
563
+ }
564
+ button {
565
+ appearance: none;
566
+ cursor: pointer;
567
+ font: inherit;
568
+ font-size: 14px;
569
+ padding: 8px 16px;
570
+ border-radius: var(--crediball-radius, 9999px);
571
+ white-space: nowrap;
572
+ }
573
+ button.copy { border: 1px solid var(--crediball-hairline, #e0e0e0); background: transparent; color: var(--crediball-ink, #1d1d1f); }
574
+ button.share { border: none; background: var(--crediball-accent, #0066cc); color: var(--crediball-on-accent, #ffffff); }
575
+ .stats { display: flex; gap: 16px; font-size: 13px; color: var(--crediball-ink-muted, #7a7a7a); }
576
+ .stats strong { color: var(--crediball-ink, #1d1d1f); font-weight: 600; }
577
+ `;
578
+ var CrediballReferralCardElement = class extends CrediballElement {
579
+ constructor() {
580
+ super();
581
+ this.data = null;
582
+ this.copied = false;
583
+ this.copiedTimer = null;
584
+ this.shadow = this.attachShadow({ mode: "open" });
585
+ }
586
+ static get observedAttributes() {
587
+ return [
588
+ ...CrediballElement.observedAttributes,
589
+ "title",
590
+ "description",
591
+ "copy-label",
592
+ "share-label",
593
+ "hide-stats"
594
+ ];
595
+ }
596
+ connectedCallback() {
597
+ super.connectedCallback();
598
+ void this.loadReferral();
599
+ }
600
+ disconnectedCallback() {
601
+ super.disconnectedCallback();
602
+ if (this.copiedTimer) clearTimeout(this.copiedTimer);
603
+ }
604
+ attributeChangedCallback(name) {
605
+ super.attributeChangedCallback(name);
606
+ if (!this.isConnected) return;
607
+ if (name === "publishable-key" || name === "user-id" || name === "api-url") {
608
+ void this.loadReferral();
609
+ }
610
+ }
611
+ /** Auto-capture ?ref=, auto-complete once a user is known, then fetch the card data. */
612
+ async loadReferral() {
613
+ const publishableKey = this.getAttribute("publishable-key");
614
+ const userId = this.getAttribute("user-id");
615
+ const apiUrl = this.getAttribute("api-url") ?? void 0;
616
+ if (!publishableKey) return;
617
+ if (this.getAttribute("capture-referrals") !== "false") {
618
+ captureReferral({ publishableKey, apiUrl });
619
+ if (userId) {
620
+ void completeStoredReferral({ publishableKey, userId, apiUrl }).catch(() => {
621
+ });
622
+ }
623
+ }
624
+ if (!userId) {
625
+ this.data = null;
626
+ this.render();
627
+ return;
628
+ }
629
+ try {
630
+ this.data = await fetchReferral2({ publishableKey, userId, apiUrl });
631
+ } catch {
632
+ this.data = null;
633
+ }
634
+ this.render();
635
+ }
636
+ render() {
637
+ const d = this.data;
638
+ if (!d || !d.enabled || !d.code) {
639
+ this.shadow.innerHTML = "";
640
+ return;
641
+ }
642
+ const title = this.getAttribute("title") ?? "Invite friends";
643
+ const copyLabel = this.getAttribute("copy-label") ?? "Copy";
644
+ const shareLabel = this.getAttribute("share-label") ?? "Share";
645
+ const hideStats = this.getAttribute("hide-stats") != null;
646
+ const reward = d.rewardCredits ?? 0;
647
+ const referredReward = d.referredRewardCredits ?? 0;
648
+ 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);
649
+ const shown = d.link ?? d.code;
650
+ const rewards = d.rewards ?? { pending: 0, converted: 0, creditsEarned: 0 };
651
+ const canShare = typeof navigator !== "undefined" && typeof navigator.share === "function";
652
+ this.shadow.innerHTML = `
653
+ <style>${STYLE2}</style>
654
+ <div class="card" part="card">
655
+ <div>
656
+ <div class="title">${escapeHtml2(title)}</div>
657
+ ${description ? `<div class="desc">${escapeHtml2(description)}</div>` : ""}
658
+ </div>
659
+ <div class="row">
660
+ <div class="link" part="link" title="${escapeHtml2(shown)}">${escapeHtml2(shown)}</div>
661
+ <button class="copy" part="copy-button" type="button">${escapeHtml2(this.copied ? "Copied!" : copyLabel)}</button>
662
+ ${canShare ? `<button class="share" part="share-button" type="button">${escapeHtml2(shareLabel)}</button>` : ""}
663
+ </div>
664
+ ${hideStats ? "" : `<div class="stats">
665
+ <span><strong>${d.clicks ?? 0}</strong> clicks</span>
666
+ <span><strong>${rewards.converted}</strong> joined</span>
667
+ ${rewards.creditsEarned > 0 ? `<span><strong>${formatCredits(rewards.creditsEarned)}</strong> credits earned</span>` : ""}
668
+ </div>`}
669
+ </div>
670
+ `;
671
+ this.shadow.querySelector("button.copy")?.addEventListener("click", () => void this.copy());
672
+ this.shadow.querySelector("button.share")?.addEventListener("click", () => void this.share());
673
+ }
674
+ linkOrCode() {
675
+ return this.data?.link ?? this.data?.code ?? null;
676
+ }
677
+ async copy() {
678
+ const text = this.linkOrCode();
679
+ if (!text || typeof navigator === "undefined" || !navigator.clipboard) return;
680
+ await navigator.clipboard.writeText(text);
681
+ this.copied = true;
682
+ this.render();
683
+ if (this.copiedTimer) clearTimeout(this.copiedTimer);
684
+ this.copiedTimer = setTimeout(() => {
685
+ this.copied = false;
686
+ this.render();
687
+ }, 2e3);
688
+ }
689
+ async share() {
690
+ const url = this.data?.link ?? void 0;
691
+ if (url && typeof navigator !== "undefined" && typeof navigator.share === "function") {
692
+ try {
693
+ await navigator.share({ url });
694
+ return;
695
+ } catch {
696
+ }
697
+ }
698
+ await this.copy();
699
+ }
700
+ };
701
+
459
702
  export {
460
703
  CrediballElement,
461
704
  CrediballBalanceElement,
462
705
  CrediballUsageElement,
463
706
  CrediballTopUpButtonElement,
464
707
  CrediballLowCreditWarningElement,
465
- CrediballPaywallElement
708
+ CrediballPaywallElement,
709
+ CrediballReferralCardElement
466
710
  };
@@ -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
  });
@@ -301,6 +302,69 @@ var Crediball = (() => {
301
302
  return client;
302
303
  }
303
304
 
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
+
304
368
  // ../core/dist/format.js
305
369
  function formatCredits(credits) {
306
370
  return new Intl.NumberFormat(void 0).format(Math.round(credits));
@@ -630,15 +694,59 @@ var Crediball = (() => {
630
694
  padding: 11px 22px;
631
695
  border-radius: var(--crediball-radius, 9999px);
632
696
  }
697
+ .referral { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--crediball-hairline, #e0e0e0); }
698
+ .referral .eyebrow { margin-bottom: 8px; }
699
+ .referral-row { display: flex; gap: 8px; align-items: center; }
700
+ .referral-row .link {
701
+ flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
702
+ font-size: 13px; color: var(--crediball-ink, #1d1d1f);
703
+ border: 1px solid var(--crediball-hairline, #e0e0e0);
704
+ border-radius: var(--crediball-radius, 9999px); padding: 8px 14px;
705
+ }
706
+ button.referral-copy {
707
+ flex: none; appearance: none; cursor: pointer; background: transparent;
708
+ color: var(--crediball-ink, #1d1d1f); border: 1px solid var(--crediball-hairline, #e0e0e0);
709
+ font: inherit; font-size: 14px; padding: 8px 16px; border-radius: var(--crediball-radius, 9999px);
710
+ white-space: nowrap;
711
+ }
633
712
  `;
634
713
  var CrediballPaywallElement = class extends CrediballElement {
635
- static get observedAttributes() {
636
- return [...CrediballElement.observedAttributes, "open", "title", "description", "amounts"];
637
- }
638
714
  constructor() {
639
715
  super();
716
+ this.referral = null;
717
+ this.referralCopied = false;
640
718
  this.shadow = this.attachShadow({ mode: "open" });
641
719
  }
720
+ static get observedAttributes() {
721
+ return [...CrediballElement.observedAttributes, "open", "title", "description", "amounts"];
722
+ }
723
+ connectedCallback() {
724
+ super.connectedCallback();
725
+ void this.loadReferral();
726
+ }
727
+ attributeChangedCallback(name) {
728
+ super.attributeChangedCallback(name);
729
+ if (this.isConnected && (name === "publishable-key" || name === "user-id" || name === "api-url")) {
730
+ void this.loadReferral();
731
+ }
732
+ }
733
+ /** Fetch the user's invite link for the "Refer a friend" row (best-effort). */
734
+ async loadReferral() {
735
+ const publishableKey = this.getAttribute("publishable-key");
736
+ const userId = this.getAttribute("user-id");
737
+ const apiUrl = this.getAttribute("api-url") ?? void 0;
738
+ if (!publishableKey || !userId) {
739
+ this.referral = null;
740
+ return;
741
+ }
742
+ try {
743
+ const data = await fetchReferral({ publishableKey, userId, apiUrl });
744
+ this.referral = data.enabled ? data : null;
745
+ } catch {
746
+ this.referral = null;
747
+ }
748
+ if (this.hasAttribute("open")) this.render();
749
+ }
642
750
  open() {
643
751
  this.setAttribute("open", "");
644
752
  }
@@ -704,6 +812,13 @@ var Crediball = (() => {
704
812
  <button class="custom-add" type="button">${escapeHtml(addButtonText)}</button>
705
813
  </div>` : ""}
706
814
  <p class="error" hidden></p>
815
+ ${this.referral?.link ? `<div class="referral">
816
+ <div class="eyebrow">${this.referral.rewardCredits && this.referral.rewardCredits > 0 ? `Refer a friend \xB7 earn ${formatCredits(this.referral.rewardCredits)} credits` : "Refer a friend"}</div>
817
+ <div class="referral-row">
818
+ <div class="link" title="${escapeHtml(this.referral.link)}">${escapeHtml(this.referral.link)}</div>
819
+ <button class="referral-copy" type="button">${this.referralCopied ? "Copied!" : "Copy"}</button>
820
+ </div>
821
+ </div>` : ""}
707
822
  <button class="close" part="close" type="button">Not now</button>
708
823
  </div>
709
824
  </div>
@@ -712,6 +827,7 @@ var Crediball = (() => {
712
827
  if (e.target === e.currentTarget) this.close();
713
828
  });
714
829
  this.shadow.querySelector(".close")?.addEventListener("click", () => this.close());
830
+ this.shadow.querySelector(".referral-copy")?.addEventListener("click", () => void this.copyReferral());
715
831
  this.shadow.querySelectorAll("button.option").forEach((el) => {
716
832
  const btn = el;
717
833
  btn.addEventListener("click", () => {
@@ -761,6 +877,18 @@ var Crediball = (() => {
761
877
  });
762
878
  }
763
879
  }
880
+ /** Copy the invite link to the clipboard, with a 2s "Copied!" confirmation. */
881
+ async copyReferral() {
882
+ const link = this.referral?.link;
883
+ if (!link || typeof navigator === "undefined" || !navigator.clipboard) return;
884
+ await navigator.clipboard.writeText(link);
885
+ this.referralCopied = true;
886
+ this.render();
887
+ setTimeout(() => {
888
+ this.referralCopied = false;
889
+ if (this.hasAttribute("open")) this.render();
890
+ }, 2e3);
891
+ }
764
892
  /** Dispatch a cancelable option event; returns true when the host did NOT preventDefault. */
765
893
  emit(name, detail) {
766
894
  const ev = new CustomEvent(name, { detail, bubbles: true, composed: true, cancelable: true });
@@ -803,6 +931,180 @@ var Crediball = (() => {
803
931
  }
804
932
  }
805
933
 
934
+ // src/referral-card.ts
935
+ var HTML_ESCAPES2 = {
936
+ "&": "&amp;",
937
+ "<": "&lt;",
938
+ ">": "&gt;",
939
+ '"': "&quot;",
940
+ "'": "&#39;"
941
+ };
942
+ function escapeHtml2(s) {
943
+ return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES2[c] ?? c);
944
+ }
945
+ var STYLE2 = `
946
+ :host { font: var(--crediball-font, system-ui, -apple-system, sans-serif); color: var(--crediball-ink, #1d1d1f); }
947
+ .card {
948
+ display: flex;
949
+ flex-direction: column;
950
+ gap: 14px;
951
+ padding: 24px;
952
+ border: 1px solid var(--crediball-hairline, #e0e0e0);
953
+ border-radius: var(--crediball-radius-card, 18px);
954
+ background: var(--crediball-canvas, #ffffff);
955
+ }
956
+ .title { font-size: 16px; font-weight: 600; }
957
+ .desc { font-size: 13px; color: var(--crediball-ink-muted, #7a7a7a); margin-top: 2px; }
958
+ .row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
959
+ .link {
960
+ flex: 1 1 180px;
961
+ min-width: 0;
962
+ overflow: hidden;
963
+ text-overflow: ellipsis;
964
+ white-space: nowrap;
965
+ font-size: 14px;
966
+ border: 1px solid var(--crediball-hairline, #e0e0e0);
967
+ border-radius: var(--crediball-radius, 9999px);
968
+ padding: 8px 14px;
969
+ }
970
+ button {
971
+ appearance: none;
972
+ cursor: pointer;
973
+ font: inherit;
974
+ font-size: 14px;
975
+ padding: 8px 16px;
976
+ border-radius: var(--crediball-radius, 9999px);
977
+ white-space: nowrap;
978
+ }
979
+ button.copy { border: 1px solid var(--crediball-hairline, #e0e0e0); background: transparent; color: var(--crediball-ink, #1d1d1f); }
980
+ button.share { border: none; background: var(--crediball-accent, #0066cc); color: var(--crediball-on-accent, #ffffff); }
981
+ .stats { display: flex; gap: 16px; font-size: 13px; color: var(--crediball-ink-muted, #7a7a7a); }
982
+ .stats strong { color: var(--crediball-ink, #1d1d1f); font-weight: 600; }
983
+ `;
984
+ var CrediballReferralCardElement = class extends CrediballElement {
985
+ constructor() {
986
+ super();
987
+ this.data = null;
988
+ this.copied = false;
989
+ this.copiedTimer = null;
990
+ this.shadow = this.attachShadow({ mode: "open" });
991
+ }
992
+ static get observedAttributes() {
993
+ return [
994
+ ...CrediballElement.observedAttributes,
995
+ "title",
996
+ "description",
997
+ "copy-label",
998
+ "share-label",
999
+ "hide-stats"
1000
+ ];
1001
+ }
1002
+ connectedCallback() {
1003
+ super.connectedCallback();
1004
+ void this.loadReferral();
1005
+ }
1006
+ disconnectedCallback() {
1007
+ super.disconnectedCallback();
1008
+ if (this.copiedTimer) clearTimeout(this.copiedTimer);
1009
+ }
1010
+ attributeChangedCallback(name) {
1011
+ super.attributeChangedCallback(name);
1012
+ if (!this.isConnected) return;
1013
+ if (name === "publishable-key" || name === "user-id" || name === "api-url") {
1014
+ void this.loadReferral();
1015
+ }
1016
+ }
1017
+ /** Auto-capture ?ref=, auto-complete once a user is known, then fetch the card data. */
1018
+ async loadReferral() {
1019
+ const publishableKey = this.getAttribute("publishable-key");
1020
+ const userId = this.getAttribute("user-id");
1021
+ const apiUrl = this.getAttribute("api-url") ?? void 0;
1022
+ if (!publishableKey) return;
1023
+ if (this.getAttribute("capture-referrals") !== "false") {
1024
+ captureReferral({ publishableKey, apiUrl });
1025
+ if (userId) {
1026
+ void completeStoredReferral({ publishableKey, userId, apiUrl }).catch(() => {
1027
+ });
1028
+ }
1029
+ }
1030
+ if (!userId) {
1031
+ this.data = null;
1032
+ this.render();
1033
+ return;
1034
+ }
1035
+ try {
1036
+ this.data = await fetchReferral({ publishableKey, userId, apiUrl });
1037
+ } catch {
1038
+ this.data = null;
1039
+ }
1040
+ this.render();
1041
+ }
1042
+ render() {
1043
+ const d = this.data;
1044
+ if (!d || !d.enabled || !d.code) {
1045
+ this.shadow.innerHTML = "";
1046
+ return;
1047
+ }
1048
+ const title = this.getAttribute("title") ?? "Invite friends";
1049
+ const copyLabel = this.getAttribute("copy-label") ?? "Copy";
1050
+ const shareLabel = this.getAttribute("share-label") ?? "Share";
1051
+ const hideStats = this.getAttribute("hide-stats") != null;
1052
+ const reward = d.rewardCredits ?? 0;
1053
+ const referredReward = d.referredRewardCredits ?? 0;
1054
+ 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);
1055
+ const shown = d.link ?? d.code;
1056
+ const rewards = d.rewards ?? { pending: 0, converted: 0, creditsEarned: 0 };
1057
+ const canShare = typeof navigator !== "undefined" && typeof navigator.share === "function";
1058
+ this.shadow.innerHTML = `
1059
+ <style>${STYLE2}</style>
1060
+ <div class="card" part="card">
1061
+ <div>
1062
+ <div class="title">${escapeHtml2(title)}</div>
1063
+ ${description ? `<div class="desc">${escapeHtml2(description)}</div>` : ""}
1064
+ </div>
1065
+ <div class="row">
1066
+ <div class="link" part="link" title="${escapeHtml2(shown)}">${escapeHtml2(shown)}</div>
1067
+ <button class="copy" part="copy-button" type="button">${escapeHtml2(this.copied ? "Copied!" : copyLabel)}</button>
1068
+ ${canShare ? `<button class="share" part="share-button" type="button">${escapeHtml2(shareLabel)}</button>` : ""}
1069
+ </div>
1070
+ ${hideStats ? "" : `<div class="stats">
1071
+ <span><strong>${d.clicks ?? 0}</strong> clicks</span>
1072
+ <span><strong>${rewards.converted}</strong> joined</span>
1073
+ ${rewards.creditsEarned > 0 ? `<span><strong>${formatCredits(rewards.creditsEarned)}</strong> credits earned</span>` : ""}
1074
+ </div>`}
1075
+ </div>
1076
+ `;
1077
+ this.shadow.querySelector("button.copy")?.addEventListener("click", () => void this.copy());
1078
+ this.shadow.querySelector("button.share")?.addEventListener("click", () => void this.share());
1079
+ }
1080
+ linkOrCode() {
1081
+ return this.data?.link ?? this.data?.code ?? null;
1082
+ }
1083
+ async copy() {
1084
+ const text = this.linkOrCode();
1085
+ if (!text || typeof navigator === "undefined" || !navigator.clipboard) return;
1086
+ await navigator.clipboard.writeText(text);
1087
+ this.copied = true;
1088
+ this.render();
1089
+ if (this.copiedTimer) clearTimeout(this.copiedTimer);
1090
+ this.copiedTimer = setTimeout(() => {
1091
+ this.copied = false;
1092
+ this.render();
1093
+ }, 2e3);
1094
+ }
1095
+ async share() {
1096
+ const url = this.data?.link ?? void 0;
1097
+ if (url && typeof navigator !== "undefined" && typeof navigator.share === "function") {
1098
+ try {
1099
+ await navigator.share({ url });
1100
+ return;
1101
+ } catch {
1102
+ }
1103
+ }
1104
+ await this.copy();
1105
+ }
1106
+ };
1107
+
806
1108
  // src/register.ts
807
1109
  function define(name, ctor) {
808
1110
  if (!customElements.get(name)) customElements.define(name, ctor);
@@ -812,5 +1114,6 @@ var Crediball = (() => {
812
1114
  define("crediball-topup-button", CrediballTopUpButtonElement);
813
1115
  define("crediball-low-credit-warning", CrediballLowCreditWarningElement);
814
1116
  define("crediball-paywall", CrediballPaywallElement);
1117
+ define("crediball-referral-card", CrediballReferralCardElement);
815
1118
  return __toCommonJS(iife_exports);
816
1119
  })();
package/dist/index.d.ts CHANGED
@@ -110,11 +110,19 @@ declare class CrediballLowCreditWarningElement extends CrediballElement {
110
110
  */
111
111
  declare class CrediballPaywallElement extends CrediballElement {
112
112
  private shadow;
113
+ private referral;
114
+ private referralCopied;
113
115
  static get observedAttributes(): string[];
114
116
  constructor();
117
+ connectedCallback(): void;
118
+ attributeChangedCallback(name: string): void;
119
+ /** Fetch the user's invite link for the "Refer a friend" row (best-effort). */
120
+ private loadReferral;
115
121
  open(): void;
116
122
  close(): void;
117
123
  protected render(): void;
124
+ /** Copy the invite link to the clipboard, with a 2s "Copied!" confirmation. */
125
+ private copyReferral;
118
126
  /** Dispatch a cancelable option event; returns true when the host did NOT preventDefault. */
119
127
  private emit;
120
128
  private returnPath;
@@ -125,4 +133,39 @@ declare class CrediballPaywallElement extends CrediballElement {
125
133
  private redirect;
126
134
  }
127
135
 
128
- export { CrediballBalanceElement, CrediballElement, CrediballLowCreditWarningElement, CrediballPaywallElement, CrediballTopUpButtonElement, CrediballUsageElement };
136
+ /**
137
+ * <crediball-referral-card publishable-key="cb_pub_..." user-id="u123"></crediball-referral-card>
138
+ *
139
+ * Drop-in invite card — the web-component sibling of @crediball/react's
140
+ * <ReferralCard/>, wrapping the same @crediball/core referral functions:
141
+ * - lazily creates + fetches the user's referral code/link/stats,
142
+ * - AUTO-CAPTURES a `?ref=CODE` visit into localStorage and, once a user-id is
143
+ * present, attaches it as a pending referral (set `capture-referrals="false"`
144
+ * to opt out and drive capture/completion yourself),
145
+ * - renders nothing while loading or when the referral program is disabled in
146
+ * the dashboard.
147
+ *
148
+ * Rewards are only ever granted server-side, when the dashboard-configured
149
+ * conversion condition is observed on a secret-key call — a browser can't
150
+ * trigger a payout. Optional attributes: title, description, copy-label,
151
+ * share-label, hide-stats.
152
+ */
153
+ declare class CrediballReferralCardElement extends CrediballElement {
154
+ private shadow;
155
+ private data;
156
+ private copied;
157
+ private copiedTimer;
158
+ static get observedAttributes(): string[];
159
+ constructor();
160
+ connectedCallback(): void;
161
+ disconnectedCallback(): void;
162
+ attributeChangedCallback(name: string): void;
163
+ /** Auto-capture ?ref=, auto-complete once a user is known, then fetch the card data. */
164
+ private loadReferral;
165
+ protected render(): void;
166
+ private linkOrCode;
167
+ private copy;
168
+ private share;
169
+ }
170
+
171
+ 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-WUPVPARI.js";
9
+ } from "./chunk-MWB56UEW.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-WUPVPARI.js";
8
+ } from "./chunk-MWB56UEW.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.1",
3
+ "version": "0.6.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>",