@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.
- package/dist/{chunk-WUPVPARI.js → chunk-MWB56UEW.js} +249 -5
- package/dist/crediball-elements.iife.js +306 -3
- package/dist/index.d.ts +44 -1
- package/dist/index.js +3 -1
- package/dist/register.js +3 -1
- package/package.json +1 -1
|
@@ -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
|
+
"&": "&",
|
|
531
|
+
"<": "<",
|
|
532
|
+
">": ">",
|
|
533
|
+
'"': """,
|
|
534
|
+
"'": "'"
|
|
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
|
+
"&": "&",
|
|
937
|
+
"<": "<",
|
|
938
|
+
">": ">",
|
|
939
|
+
'"': """,
|
|
940
|
+
"'": "'"
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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