@crediball/elements 0.7.0 → 0.9.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.
@@ -187,6 +187,7 @@ var CrediballLowCreditWarningElement = class extends CrediballElement {
187
187
  // src/paywall.ts
188
188
  import { formatMoney, fetchReferral } from "@crediball/core";
189
189
  var CURRENCY_SYMBOLS = { EUR: "\u20AC", USD: "$", GBP: "\xA3" };
190
+ var PERIOD_SUFFIX = { weekly: "wk", monthly: "mo", yearly: "yr" };
190
191
  function currencySymbolFor(currency) {
191
192
  return CURRENCY_SYMBOLS[currency.toUpperCase()] ?? currency;
192
193
  }
@@ -269,6 +270,7 @@ var STYLE = `
269
270
  padding: 10px 18px;
270
271
  border-radius: var(--crediball-radius, 9999px);
271
272
  }
273
+ p.custom-preview { margin: 6px 0 0; font-size: 12px; color: var(--crediball-ink-muted, #7a7a7a); }
272
274
  p.error { margin: 8px 0 0; font-size: 13px; color: var(--crediball-accent, #0066cc); }
273
275
  button.close {
274
276
  width: 100%;
@@ -283,6 +285,25 @@ var STYLE = `
283
285
  padding: 11px 22px;
284
286
  border-radius: var(--crediball-radius, 9999px);
285
287
  }
288
+ .promo-code { margin-top: 16px; }
289
+ .promo-code-toggle {
290
+ appearance: none; border: none; cursor: pointer; background: transparent; padding: 0;
291
+ font: inherit; font-size: 12px; color: var(--crediball-ink-muted, #7a7a7a); text-decoration: underline;
292
+ }
293
+ .promo-code-label { margin-bottom: 6px; font-size: 12px; font-weight: 600; color: var(--crediball-ink-muted, #7a7a7a); }
294
+ .promo-code-input {
295
+ width: 100%;
296
+ box-sizing: border-box;
297
+ appearance: none;
298
+ font: inherit;
299
+ font-size: 14px;
300
+ padding: 8px 12px;
301
+ border-radius: var(--crediball-radius, 9999px);
302
+ border: 1px solid var(--crediball-hairline, #e0e0e0);
303
+ background: var(--crediball-canvas, #ffffff);
304
+ color: var(--crediball-ink, #1d1d1f);
305
+ outline: none;
306
+ }
286
307
  .autotopup { margin-top: 16px; }
287
308
  .autotopup-toggle {
288
309
  appearance: none; border: none; cursor: pointer; background: transparent; padding: 0;
@@ -326,10 +347,19 @@ var CrediballPaywallElement = class extends CrediballElement {
326
347
  this.referral = null;
327
348
  this.referralCopied = false;
328
349
  this.autoTopupOpen = false;
350
+ this.promoCodeOpen = false;
329
351
  this.shadow = this.attachShadow({ mode: "open" });
330
352
  }
331
353
  static get observedAttributes() {
332
- return [...CrediballElement.observedAttributes, "open", "title", "description", "amounts"];
354
+ return [
355
+ ...CrediballElement.observedAttributes,
356
+ "open",
357
+ "title",
358
+ "description",
359
+ "amounts",
360
+ "allow-promo-code",
361
+ "promo-code-label"
362
+ ];
333
363
  }
334
364
  connectedCallback() {
335
365
  super.connectedCallback();
@@ -399,10 +429,11 @@ var CrediballPaywallElement = class extends CrediballElement {
399
429
  (p) => `
400
430
  <button class="option" data-kind="plan" data-id="${escapeHtml(p.id)}">
401
431
  <span>${escapeHtml(p.name)}</span>
402
- <span class="meta">${formatMoney(p.price, currency)}/${p.period === "monthly" ? "mo" : "yr"} \xB7 ${formatCredits(p.credits)} cr</span>
432
+ <span class="meta">${formatMoney(p.price, currency)}/${PERIOD_SUFFIX[p.period] ?? p.period} \xB7 ${formatCredits(p.credits)} cr</span>
403
433
  </button>`
404
434
  ).join("")}
405
435
  </div>` : ""}
436
+ ${this.renderPromoCode()}
406
437
  <div class="list">
407
438
  ${packages.length ? packages.map(
408
439
  (p) => `
@@ -421,7 +452,8 @@ var CrediballPaywallElement = class extends CrediballElement {
421
452
  <span class="sym">${escapeHtml(currencySymbolFor(currency))}</span>
422
453
  <input class="custom-input" inputmode="decimal" placeholder="${custom?.minEur ?? ""}" aria-label="Custom amount" />
423
454
  <button class="custom-add" type="button">${escapeHtml(addButtonText)}</button>
424
- </div>` : ""}
455
+ </div>
456
+ <p class="custom-preview" hidden></p>` : ""}
425
457
  ${this.renderAutoTopup(packages, pkgs?.autoTopup, currency)}
426
458
  <p class="error" hidden></p>
427
459
  <button class="close" part="close" type="button">Not now</button>
@@ -451,8 +483,9 @@ var CrediballPaywallElement = class extends CrediballElement {
451
483
  const kind = btn.dataset.kind;
452
484
  if (kind === "package") {
453
485
  const pkg = packages.find((p) => p.id === btn.dataset.id);
454
- if (pkg && this.emit("crediball-select-package", pkg)) {
455
- void this.startCheckout({ packageId: pkg.id });
486
+ const code = this.currentPromoCode();
487
+ if (pkg && this.emit("crediball-select-package", { ...pkg, code })) {
488
+ void this.startCheckout({ packageId: pkg.id, code });
456
489
  }
457
490
  } else if (kind === "plan") {
458
491
  const plan = plans.find((p) => p.id === btn.dataset.id);
@@ -461,16 +494,33 @@ var CrediballPaywallElement = class extends CrediballElement {
461
494
  }
462
495
  } else if (kind === "amount") {
463
496
  const amount = Number(btn.dataset.amount);
464
- if (this.emit("crediball-topup", { amount })) {
465
- void this.startCheckout({ eurAmount: amount });
497
+ const code = this.currentPromoCode();
498
+ if (this.emit("crediball-topup", { amount, code })) {
499
+ void this.startCheckout({ eurAmount: amount, code });
466
500
  }
467
501
  }
468
502
  });
469
503
  });
504
+ this.shadow.querySelector("[data-open-promo-code]")?.addEventListener("click", () => {
505
+ this.promoCodeOpen = true;
506
+ this.render();
507
+ });
470
508
  if (showCustom) {
471
509
  const input = this.shadow.querySelector(".custom-input");
472
510
  const addBtn = this.shadow.querySelector(".custom-add");
473
511
  const errEl = this.shadow.querySelector(".error");
512
+ const previewEl = this.shadow.querySelector(".custom-preview");
513
+ const rate = custom?.rate;
514
+ input?.addEventListener("input", () => {
515
+ if (!previewEl) return;
516
+ const amount = Number(input.value);
517
+ if (!rate || rate <= 0 || !Number.isFinite(amount) || amount <= 0) {
518
+ previewEl.hidden = true;
519
+ return;
520
+ }
521
+ previewEl.hidden = false;
522
+ previewEl.textContent = `\u2248 ${formatCredits(Math.round(amount * rate))} credits`;
523
+ });
474
524
  const submit = () => {
475
525
  const amount = Number(input?.value);
476
526
  const sym = currencySymbolFor(currency);
@@ -484,8 +534,9 @@ var CrediballPaywallElement = class extends CrediballElement {
484
534
  return showError(errEl, `Maximum is ${sym}${custom.maxEur}.`);
485
535
  }
486
536
  showError(errEl, null);
487
- if (this.emit("crediball-topup", { amount })) {
488
- void this.startCheckout({ eurAmount: amount });
537
+ const code = this.currentPromoCode();
538
+ if (this.emit("crediball-topup", { amount, code })) {
539
+ void this.startCheckout({ eurAmount: amount, code });
489
540
  }
490
541
  };
491
542
  addBtn?.addEventListener("click", submit);
@@ -517,6 +568,30 @@ var CrediballPaywallElement = class extends CrediballElement {
517
568
  }
518
569
  });
519
570
  }
571
+ /** HTML for the "Have a promo code?" toggle-open field. Gated on the
572
+ * `allow-promo-code` attribute (off by default). Not validated client-side —
573
+ * an invalid code simply doesn't grant a bonus when the top-up is processed. */
574
+ renderPromoCode() {
575
+ if (!this.hasAttribute("allow-promo-code")) return "";
576
+ const label = this.getAttribute("promo-code-label") ?? "Have a promo code?";
577
+ if (!this.promoCodeOpen) {
578
+ return `<div class="promo-code">
579
+ <button class="promo-code-toggle" type="button" data-open-promo-code>${escapeHtml(label)}</button>
580
+ </div>`;
581
+ }
582
+ return `<div class="promo-code">
583
+ <div class="promo-code-label">${escapeHtml(label)}</div>
584
+ <input class="promo-code-input" placeholder="PROMOCODE" aria-label="Promo code" />
585
+ </div>`;
586
+ }
587
+ /** The current promo code input value (uppercased, trimmed), or undefined
588
+ * when the field isn't shown/filled in. Read at click time — never re-render
589
+ * on keystroke, same reasoning as the custom-amount field. */
590
+ currentPromoCode() {
591
+ const input = this.shadow.querySelector(".promo-code-input");
592
+ const value = input?.value.trim().toUpperCase();
593
+ return value || void 0;
594
+ }
520
595
  /** HTML for the auto-topup opt-in row / status line / needs_auth-fix prompt. */
521
596
  renderAutoTopup(packages, autoTopup, currency) {
522
597
  if (!packages.length) return "";
@@ -601,6 +601,7 @@ var Crediball = (() => {
601
601
 
602
602
  // src/paywall.ts
603
603
  var CURRENCY_SYMBOLS = { EUR: "\u20AC", USD: "$", GBP: "\xA3" };
604
+ var PERIOD_SUFFIX = { weekly: "wk", monthly: "mo", yearly: "yr" };
604
605
  function currencySymbolFor(currency) {
605
606
  return CURRENCY_SYMBOLS[currency.toUpperCase()] ?? currency;
606
607
  }
@@ -683,6 +684,7 @@ var Crediball = (() => {
683
684
  padding: 10px 18px;
684
685
  border-radius: var(--crediball-radius, 9999px);
685
686
  }
687
+ p.custom-preview { margin: 6px 0 0; font-size: 12px; color: var(--crediball-ink-muted, #7a7a7a); }
686
688
  p.error { margin: 8px 0 0; font-size: 13px; color: var(--crediball-accent, #0066cc); }
687
689
  button.close {
688
690
  width: 100%;
@@ -697,6 +699,25 @@ var Crediball = (() => {
697
699
  padding: 11px 22px;
698
700
  border-radius: var(--crediball-radius, 9999px);
699
701
  }
702
+ .promo-code { margin-top: 16px; }
703
+ .promo-code-toggle {
704
+ appearance: none; border: none; cursor: pointer; background: transparent; padding: 0;
705
+ font: inherit; font-size: 12px; color: var(--crediball-ink-muted, #7a7a7a); text-decoration: underline;
706
+ }
707
+ .promo-code-label { margin-bottom: 6px; font-size: 12px; font-weight: 600; color: var(--crediball-ink-muted, #7a7a7a); }
708
+ .promo-code-input {
709
+ width: 100%;
710
+ box-sizing: border-box;
711
+ appearance: none;
712
+ font: inherit;
713
+ font-size: 14px;
714
+ padding: 8px 12px;
715
+ border-radius: var(--crediball-radius, 9999px);
716
+ border: 1px solid var(--crediball-hairline, #e0e0e0);
717
+ background: var(--crediball-canvas, #ffffff);
718
+ color: var(--crediball-ink, #1d1d1f);
719
+ outline: none;
720
+ }
700
721
  .autotopup { margin-top: 16px; }
701
722
  .autotopup-toggle {
702
723
  appearance: none; border: none; cursor: pointer; background: transparent; padding: 0;
@@ -740,10 +761,19 @@ var Crediball = (() => {
740
761
  this.referral = null;
741
762
  this.referralCopied = false;
742
763
  this.autoTopupOpen = false;
764
+ this.promoCodeOpen = false;
743
765
  this.shadow = this.attachShadow({ mode: "open" });
744
766
  }
745
767
  static get observedAttributes() {
746
- return [...CrediballElement.observedAttributes, "open", "title", "description", "amounts"];
768
+ return [
769
+ ...CrediballElement.observedAttributes,
770
+ "open",
771
+ "title",
772
+ "description",
773
+ "amounts",
774
+ "allow-promo-code",
775
+ "promo-code-label"
776
+ ];
747
777
  }
748
778
  connectedCallback() {
749
779
  super.connectedCallback();
@@ -813,10 +843,11 @@ var Crediball = (() => {
813
843
  (p) => `
814
844
  <button class="option" data-kind="plan" data-id="${escapeHtml(p.id)}">
815
845
  <span>${escapeHtml(p.name)}</span>
816
- <span class="meta">${formatMoney(p.price, currency)}/${p.period === "monthly" ? "mo" : "yr"} \xB7 ${formatCredits(p.credits)} cr</span>
846
+ <span class="meta">${formatMoney(p.price, currency)}/${PERIOD_SUFFIX[p.period] ?? p.period} \xB7 ${formatCredits(p.credits)} cr</span>
817
847
  </button>`
818
848
  ).join("")}
819
849
  </div>` : ""}
850
+ ${this.renderPromoCode()}
820
851
  <div class="list">
821
852
  ${packages.length ? packages.map(
822
853
  (p) => `
@@ -835,7 +866,8 @@ var Crediball = (() => {
835
866
  <span class="sym">${escapeHtml(currencySymbolFor(currency))}</span>
836
867
  <input class="custom-input" inputmode="decimal" placeholder="${custom?.minEur ?? ""}" aria-label="Custom amount" />
837
868
  <button class="custom-add" type="button">${escapeHtml(addButtonText)}</button>
838
- </div>` : ""}
869
+ </div>
870
+ <p class="custom-preview" hidden></p>` : ""}
839
871
  ${this.renderAutoTopup(packages, pkgs?.autoTopup, currency)}
840
872
  <p class="error" hidden></p>
841
873
  <button class="close" part="close" type="button">Not now</button>
@@ -865,8 +897,9 @@ var Crediball = (() => {
865
897
  const kind = btn.dataset.kind;
866
898
  if (kind === "package") {
867
899
  const pkg = packages.find((p) => p.id === btn.dataset.id);
868
- if (pkg && this.emit("crediball-select-package", pkg)) {
869
- void this.startCheckout({ packageId: pkg.id });
900
+ const code = this.currentPromoCode();
901
+ if (pkg && this.emit("crediball-select-package", { ...pkg, code })) {
902
+ void this.startCheckout({ packageId: pkg.id, code });
870
903
  }
871
904
  } else if (kind === "plan") {
872
905
  const plan = plans.find((p) => p.id === btn.dataset.id);
@@ -875,16 +908,33 @@ var Crediball = (() => {
875
908
  }
876
909
  } else if (kind === "amount") {
877
910
  const amount = Number(btn.dataset.amount);
878
- if (this.emit("crediball-topup", { amount })) {
879
- void this.startCheckout({ eurAmount: amount });
911
+ const code = this.currentPromoCode();
912
+ if (this.emit("crediball-topup", { amount, code })) {
913
+ void this.startCheckout({ eurAmount: amount, code });
880
914
  }
881
915
  }
882
916
  });
883
917
  });
918
+ this.shadow.querySelector("[data-open-promo-code]")?.addEventListener("click", () => {
919
+ this.promoCodeOpen = true;
920
+ this.render();
921
+ });
884
922
  if (showCustom) {
885
923
  const input = this.shadow.querySelector(".custom-input");
886
924
  const addBtn = this.shadow.querySelector(".custom-add");
887
925
  const errEl = this.shadow.querySelector(".error");
926
+ const previewEl = this.shadow.querySelector(".custom-preview");
927
+ const rate = custom?.rate;
928
+ input?.addEventListener("input", () => {
929
+ if (!previewEl) return;
930
+ const amount = Number(input.value);
931
+ if (!rate || rate <= 0 || !Number.isFinite(amount) || amount <= 0) {
932
+ previewEl.hidden = true;
933
+ return;
934
+ }
935
+ previewEl.hidden = false;
936
+ previewEl.textContent = `\u2248 ${formatCredits(Math.round(amount * rate))} credits`;
937
+ });
888
938
  const submit = () => {
889
939
  const amount = Number(input?.value);
890
940
  const sym = currencySymbolFor(currency);
@@ -898,8 +948,9 @@ var Crediball = (() => {
898
948
  return showError(errEl, `Maximum is ${sym}${custom.maxEur}.`);
899
949
  }
900
950
  showError(errEl, null);
901
- if (this.emit("crediball-topup", { amount })) {
902
- void this.startCheckout({ eurAmount: amount });
951
+ const code = this.currentPromoCode();
952
+ if (this.emit("crediball-topup", { amount, code })) {
953
+ void this.startCheckout({ eurAmount: amount, code });
903
954
  }
904
955
  };
905
956
  addBtn?.addEventListener("click", submit);
@@ -931,6 +982,30 @@ var Crediball = (() => {
931
982
  }
932
983
  });
933
984
  }
985
+ /** HTML for the "Have a promo code?" toggle-open field. Gated on the
986
+ * `allow-promo-code` attribute (off by default). Not validated client-side —
987
+ * an invalid code simply doesn't grant a bonus when the top-up is processed. */
988
+ renderPromoCode() {
989
+ if (!this.hasAttribute("allow-promo-code")) return "";
990
+ const label = this.getAttribute("promo-code-label") ?? "Have a promo code?";
991
+ if (!this.promoCodeOpen) {
992
+ return `<div class="promo-code">
993
+ <button class="promo-code-toggle" type="button" data-open-promo-code>${escapeHtml(label)}</button>
994
+ </div>`;
995
+ }
996
+ return `<div class="promo-code">
997
+ <div class="promo-code-label">${escapeHtml(label)}</div>
998
+ <input class="promo-code-input" placeholder="PROMOCODE" aria-label="Promo code" />
999
+ </div>`;
1000
+ }
1001
+ /** The current promo code input value (uppercased, trimmed), or undefined
1002
+ * when the field isn't shown/filled in. Read at click time — never re-render
1003
+ * on keystroke, same reasoning as the custom-amount field. */
1004
+ currentPromoCode() {
1005
+ const input = this.shadow.querySelector(".promo-code-input");
1006
+ const value = input?.value.trim().toUpperCase();
1007
+ return value || void 0;
1008
+ }
934
1009
  /** HTML for the auto-topup opt-in row / status line / needs_auth-fix prompt. */
935
1010
  renderAutoTopup(packages, autoTopup, currency) {
936
1011
  if (!packages.length) return "";
package/dist/index.d.ts CHANGED
@@ -113,6 +113,7 @@ declare class CrediballPaywallElement extends CrediballElement {
113
113
  private referral;
114
114
  private referralCopied;
115
115
  private autoTopupOpen;
116
+ private promoCodeOpen;
116
117
  static get observedAttributes(): string[];
117
118
  constructor();
118
119
  connectedCallback(): void;
@@ -122,6 +123,14 @@ declare class CrediballPaywallElement extends CrediballElement {
122
123
  open(): void;
123
124
  close(): void;
124
125
  protected render(): void;
126
+ /** HTML for the "Have a promo code?" toggle-open field. Gated on the
127
+ * `allow-promo-code` attribute (off by default). Not validated client-side —
128
+ * an invalid code simply doesn't grant a bonus when the top-up is processed. */
129
+ private renderPromoCode;
130
+ /** The current promo code input value (uppercased, trimmed), or undefined
131
+ * when the field isn't shown/filled in. Read at click time — never re-render
132
+ * on keystroke, same reasoning as the custom-amount field. */
133
+ private currentPromoCode;
125
134
  /** HTML for the auto-topup opt-in row / status line / needs_auth-fix prompt. */
126
135
  private renderAutoTopup;
127
136
  /** Copy the invite link to the clipboard, with a 2s "Copied!" confirmation. */
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  CrediballReferralCardElement,
7
7
  CrediballTopUpButtonElement,
8
8
  CrediballUsageElement
9
- } from "./chunk-KRSZ3FQQ.js";
9
+ } from "./chunk-MLNG4RJB.js";
10
10
  export {
11
11
  CrediballBalanceElement,
12
12
  CrediballElement,
package/dist/register.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  CrediballReferralCardElement,
6
6
  CrediballTopUpButtonElement,
7
7
  CrediballUsageElement
8
- } from "./chunk-KRSZ3FQQ.js";
8
+ } from "./chunk-MLNG4RJB.js";
9
9
 
10
10
  // src/register.ts
11
11
  function define(name, ctor) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crediball/elements",
3
- "version": "0.7.0",
3
+ "version": "0.9.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.6.0"
59
+ "@crediball/core": "^0.8.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "tsup": "^8.3.5",