@slyxup/ui 0.2.9 → 0.2.11

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/src/index.ts CHANGED
@@ -35,6 +35,7 @@ export {
35
35
  PricingTable,
36
36
  type PricingTableProps,
37
37
  } from './components/PricingTable/PricingTable';
38
+ export { initPaddle, openPaddleCheckout } from './lib/paddle';
38
39
 
39
40
  import { useEffect } from 'react';
40
41
  import { injectStyles } from './styles';
@@ -0,0 +1,131 @@
1
+ // Paddle.js loader — lazy-loads Paddle.js and initializes overlay checkout
2
+ // Docs: https://developer.paddle.com/paddle-js
3
+
4
+ declare global {
5
+ interface Window {
6
+ Paddle?: {
7
+ Initialize: (config: {
8
+ token: string;
9
+ eventCallback?: (data: Record<string, unknown>) => void;
10
+ }) => void;
11
+ Environment: { set: (env: 'sandbox' | 'production') => void };
12
+ Checkout: {
13
+ open: (options: {
14
+ items: Array<{ priceId: string; quantity: number }>;
15
+ customer?: { email?: string };
16
+ settings?: Record<string, unknown>;
17
+ customData?: Record<string, unknown>;
18
+ }) => void;
19
+ };
20
+ };
21
+ }
22
+ }
23
+
24
+ const PADDLE_JS_URL = 'https://cdn.paddle.com/paddle/v2/paddle.js';
25
+ let paddleLoaded = false;
26
+ let paddleInitPromise: Promise<void> | null = null;
27
+
28
+ function loadScript(src: string): Promise<void> {
29
+ return new Promise((resolve, reject) => {
30
+ if (document.querySelector(`script[src="${src}"]`)) {
31
+ resolve();
32
+ return;
33
+ }
34
+ const s = document.createElement('script');
35
+ s.src = src;
36
+ s.onload = () => resolve();
37
+ s.onerror = () => reject(new Error(`Failed to load ${src}`));
38
+ document.head.appendChild(s);
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Derive billing URL from auth API URL.
44
+ * localhost:8787 → localhost:8788, auth.slyxup.online → billing.slyxup.online
45
+ */
46
+ function deriveBillingUrl(authApiUrl: string): string {
47
+ if (/^https?:\/\/localhost(:\d+)?$/.test(authApiUrl)) {
48
+ return authApiUrl.replace(/:(\d+)$/, ':8788');
49
+ }
50
+ return authApiUrl.replace('auth.slyxup.online', 'billing.slyxup.online');
51
+ }
52
+
53
+ interface BillingConfig {
54
+ environment: 'sandbox' | 'production';
55
+ clientToken: string;
56
+ }
57
+
58
+ let cachedConfig: BillingConfig | null = null;
59
+
60
+ /**
61
+ * Fetch Paddle config from billing Worker's /v1/billing/config endpoint.
62
+ * Cached after first fetch.
63
+ */
64
+ async function fetchBillingConfig(billingUrl: string): Promise<BillingConfig> {
65
+ if (cachedConfig) return cachedConfig;
66
+ const res = await fetch(`${billingUrl}/v1/billing/config`);
67
+ const data = (await res.json().catch(() => ({}))) as {
68
+ ok?: boolean;
69
+ environment?: string;
70
+ clientToken?: string;
71
+ };
72
+ if (!res.ok || !data.ok || !data.clientToken) {
73
+ throw new Error('Failed to fetch billing config');
74
+ }
75
+ cachedConfig = {
76
+ environment: (data.environment as 'sandbox' | 'production') ?? 'sandbox',
77
+ clientToken: data.clientToken,
78
+ };
79
+ return cachedConfig;
80
+ }
81
+
82
+ /**
83
+ * Load Paddle.js and initialize with config from billing Worker.
84
+ * Safe to call multiple times — only loads the script once.
85
+ */
86
+ export async function initPaddle(authApiUrl: string): Promise<void> {
87
+ if (paddleLoaded && window.Paddle) return;
88
+
89
+ if (!paddleInitPromise) {
90
+ paddleInitPromise = (async () => {
91
+ const billingUrl = deriveBillingUrl(authApiUrl);
92
+ const config = await fetchBillingConfig(billingUrl);
93
+ await loadScript(PADDLE_JS_URL);
94
+ if (!window.Paddle) throw new Error('Paddle.js failed to load');
95
+ window.Paddle.Environment.set(config.environment);
96
+ window.Paddle.Initialize({
97
+ token: config.clientToken,
98
+ eventCallback: (data) => {
99
+ if (data && data.name === 'checkout.completed') {
100
+ // Subscription is created via webhook. Notify the host app so it can
101
+ // refresh billing state and show a success confirmation.
102
+ window.dispatchEvent(new CustomEvent('slyxup:checkout-completed'));
103
+ }
104
+ },
105
+ });
106
+ paddleLoaded = true;
107
+ })();
108
+ }
109
+ return paddleInitPromise;
110
+ }
111
+
112
+ /**
113
+ * Open Paddle overlay checkout for a given price ID.
114
+ * `customData` is copied to the created transaction (and, for recurring
115
+ * items, to the related subscription) — the billing webhook uses it to
116
+ * attribute the subscription to a SlyxUp user + project + plan.
117
+ */
118
+ export function openPaddleCheckout(
119
+ priceId: string,
120
+ customerEmail?: string,
121
+ customData?: Record<string, unknown>
122
+ ): void {
123
+ if (!window.Paddle) {
124
+ throw new Error('Paddle.js not initialized — call initPaddle() first');
125
+ }
126
+ window.Paddle.Checkout.open({
127
+ items: [{ priceId, quantity: 1 }],
128
+ ...(customerEmail ? { customer: { email: customerEmail } } : {}),
129
+ ...(customData ? { customData } : {}),
130
+ });
131
+ }
package/src/styles.ts CHANGED
@@ -443,6 +443,14 @@ export const CSS = `
443
443
  .slx-session-device { font-size: 13.5px; font-weight: 550; color: var(--slx-ink); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
444
444
  .slx-session-sub { font-size: 12px; color: var(--slx-muted); margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
445
445
 
446
+ /* ── Pagination ── */
447
+ .slx-pagination {
448
+ display: flex; align-items: center; justify-content: center; gap: 10px;
449
+ margin-top: 14px; padding-top: 14px;
450
+ border-top: 1px solid var(--slx-border);
451
+ }
452
+ .slx-pagination-info { font-size: 12.5px; color: var(--slx-muted); white-space: nowrap; }
453
+
446
454
  /* ── Danger zone ── */
447
455
  .slx-danger-zone {
448
456
  border: 1px solid color-mix(in srgb, var(--slx-danger) 30%, transparent);
@@ -570,6 +578,8 @@ export const CSS = `
570
578
  .slx-billing-plans { grid-template-columns: 1fr; }
571
579
  .slx-session { flex-direction: column; align-items: flex-start; gap: 10px; }
572
580
  .slx-session .slx-btn-danger-outline { align-self: stretch; text-align: center; justify-content: center; }
581
+ .slx-pagination { flex-wrap: wrap; gap: 6px; }
582
+ .slx-pagination .slx-btn-secondary { font-size: 12px; padding: 4px 10px; }
573
583
  .slx-invoice-row { flex-wrap: wrap; gap: 6px; }
574
584
  }
575
585
  @media (max-width: 380px) {