@adia-ai/web-modules 0.8.35 → 0.8.37
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/CHANGELOG.md +22 -0
- package/README.md +3 -3
- package/agent-admin/admin-roster/admin-roster.js +1 -1
- package/agent-admin/admin-settings/admin-settings.js +1 -1
- package/agent-admin/agent-admin/agent-admin.js +1 -1
- package/billing/billing-overview/billing-overview.a2ui.json +281 -0
- package/billing/billing-overview/billing-overview.class.js +838 -0
- package/billing/billing-overview/billing-overview.css +223 -0
- package/billing/billing-overview/billing-overview.js +16 -0
- package/billing/billing-overview/billing-overview.yaml +441 -0
- package/billing/index.js +21 -0
- package/billing/invoice-detail/invoice-detail.a2ui.json +276 -0
- package/billing/invoice-detail/invoice-detail.class.js +711 -0
- package/billing/invoice-detail/invoice-detail.css +305 -0
- package/billing/invoice-detail/invoice-detail.js +16 -0
- package/billing/invoice-detail/invoice-detail.yaml +411 -0
- package/billing/invoice-history/invoice-history.a2ui.json +315 -0
- package/billing/invoice-history/invoice-history.class.js +672 -0
- package/billing/invoice-history/invoice-history.css +130 -0
- package/billing/invoice-history/invoice-history.js +16 -0
- package/billing/invoice-history/invoice-history.yaml +398 -0
- package/billing/payment-method-form/payment-method-form.a2ui.json +310 -0
- package/billing/payment-method-form/payment-method-form.class.js +886 -0
- package/billing/payment-method-form/payment-method-form.css +149 -0
- package/billing/payment-method-form/payment-method-form.js +16 -0
- package/billing/payment-method-form/payment-method-form.yaml +430 -0
- package/billing/payment-method-list/payment-method-list.a2ui.json +283 -0
- package/billing/payment-method-list/payment-method-list.class.js +705 -0
- package/billing/payment-method-list/payment-method-list.css +128 -0
- package/billing/payment-method-list/payment-method-list.js +16 -0
- package/billing/payment-method-list/payment-method-list.yaml +355 -0
- package/billing/plan-picker/plan-picker.a2ui.json +349 -0
- package/billing/plan-picker/plan-picker.class.js +635 -0
- package/billing/plan-picker/plan-picker.css +227 -0
- package/billing/plan-picker/plan-picker.js +16 -0
- package/billing/plan-picker/plan-picker.yaml +453 -0
- package/chat/README.md +1 -1
- package/chat/chat-composer/chat-composer.js +2 -2
- package/chat/chat-empty/chat-empty.js +2 -2
- package/chat/chat-shell/chat-shell.js +4 -4
- package/chat/chat-sidebar/chat-sidebar.js +2 -2
- package/chat/chat-surfaces/chat-surfaces.js +2 -2
- package/chat/chat-thread/chat-thread.js +2 -2
- package/dashboard/dashboard-layout/dashboard-layout.a2ui.json +254 -0
- package/dashboard/dashboard-layout/dashboard-layout.css +190 -0
- package/dashboard/dashboard-layout/dashboard-layout.js +289 -0
- package/dashboard/dashboard-layout/dashboard-layout.yaml +374 -0
- package/dashboard/date-range-selector/date-range-selector.a2ui.json +236 -0
- package/dashboard/date-range-selector/date-range-selector.css +84 -0
- package/dashboard/date-range-selector/date-range-selector.js +686 -0
- package/dashboard/date-range-selector/date-range-selector.yaml +358 -0
- package/dashboard/index.js +14 -0
- package/dist/everything.min.js +1 -1
- package/editor/editor-canvas/editor-canvas.js +2 -2
- package/editor/editor-shell/editor-shell.js +2 -2
- package/editor/editor-sidebar/editor-sidebar.js +2 -2
- package/editor/editor-toolbar/editor-toolbar.js +2 -2
- package/feedback/confirm-dialog/confirm-dialog.a2ui.json +206 -0
- package/feedback/confirm-dialog/confirm-dialog.css +157 -0
- package/feedback/confirm-dialog/confirm-dialog.js +383 -0
- package/feedback/confirm-dialog/confirm-dialog.yaml +297 -0
- package/feedback/index.js +10 -0
- package/form/form-popover/form-popover.js +1 -1
- package/generative/index.d.ts +1 -1
- package/generative/index.js +2 -2
- package/onboarding/index.js +1 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.a2ui.json +263 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.css +157 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.js +428 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.yaml +236 -0
- package/package.json +99 -6
- package/runtime/a2ui-root/a2ui-root.js +3 -3
- package/runtime/gen-root/gen-root.js +5 -5
- package/settings/index.js +12 -0
- package/settings/integrations-page/integrations-page.a2ui.json +274 -0
- package/settings/integrations-page/integrations-page.css +177 -0
- package/settings/integrations-page/integrations-page.d.ts +77 -0
- package/settings/integrations-page/integrations-page.js +413 -0
- package/settings/integrations-page/integrations-page.yaml +317 -0
- package/settings/notification-preferences/notification-preferences.a2ui.json +245 -0
- package/settings/notification-preferences/notification-preferences.css +204 -0
- package/settings/notification-preferences/notification-preferences.js +622 -0
- package/settings/notification-preferences/notification-preferences.yaml +358 -0
- package/shell/admin-command/admin-command.js +2 -2
- package/shell/admin-shell/admin-shell.js +2 -2
- package/shell/admin-sidebar/admin-sidebar.js +2 -2
- package/shell/embed-shell/embed-shell.js +3 -3
- package/simple/simple-shell/simple-shell.js +1 -1
- package/theme/theme-panel/theme-panel.js +2 -2
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <billing-overview-ui> — Module-tier billing dashboard composite.
|
|
3
|
+
*
|
|
4
|
+
* Stitches the Wave A+B billing primitives into a single dashboard
|
|
5
|
+
* surface — header (current plan + status badge) → KPI strip
|
|
6
|
+
* (current spend · seats · next invoice) → optional dunning banner
|
|
7
|
+
* (when past-due) → plan-picker section → payment-method-list section
|
|
8
|
+
* → invoice-history section. The composite owns the page-level
|
|
9
|
+
* layout, status-badge variant mapping, KPI formatting, and the
|
|
10
|
+
* unified `account-action` event surface. Each child primitive owns
|
|
11
|
+
* its own row chrome.
|
|
12
|
+
*
|
|
13
|
+
* SPEC: .claude/docs/specs/implementation-ready/SPEC-004-billing-overview.md
|
|
14
|
+
* Wave A+B siblings composed:
|
|
15
|
+
* - SPEC-006 — alert-ui[pattern=dunning] for past-due state
|
|
16
|
+
* - SPEC-008 — invoice-history-ui for the invoice list section
|
|
17
|
+
* - SPEC-010 — payment-method-list-ui for the cards/wallets section
|
|
18
|
+
* - SPEC-011 — plan-picker-ui for the in-app plan chooser
|
|
19
|
+
* - stat-ui for the KPI strip
|
|
20
|
+
*
|
|
21
|
+
* Architecture (light-DOM, ADR-0033):
|
|
22
|
+
* Stamps a six-region skeleton once on first connect:
|
|
23
|
+
* [data-header] — plan title + status tag + slotted actions
|
|
24
|
+
* [data-kpis] — 3-stat KPI strip
|
|
25
|
+
* [data-dunning] — alert-ui[pattern="dunning"], conditional
|
|
26
|
+
* [data-plan-picker] — plan-picker-ui section, conditional
|
|
27
|
+
* [data-payment-methods] — payment-method-list-ui section
|
|
28
|
+
* [data-invoice-history] — invoice-history-ui section
|
|
29
|
+
* [data-empty] — default empty-state branch
|
|
30
|
+
* Then mutates per-region nodes via property assignment on every
|
|
31
|
+
* `account` / `variant` / `currency` / `loading` change. Children
|
|
32
|
+
* are real custom-element instances; the composite forwards data
|
|
33
|
+
* into their `.plans` / `.methods` / `.invoices` properties.
|
|
34
|
+
*
|
|
35
|
+
* Form participation: none. Extends UIElement, not UIFormElement —
|
|
36
|
+
* the dashboard is a display surface. Child primitives (plan-picker,
|
|
37
|
+
* payment-method-list) are individually form-bearing.
|
|
38
|
+
*
|
|
39
|
+
* Per ADR-0027 composed primitives are NOT auto-imported. The
|
|
40
|
+
* consumer (or this composite's .html demo shell) must side-effect
|
|
41
|
+
* import each one explicitly.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { UIElement } from '@adia-ai/web-components/core/element';
|
|
45
|
+
import { untracked } from '@adia-ai/web-components/core/signals';
|
|
46
|
+
|
|
47
|
+
const KNOWN_STATUSES = new Set([
|
|
48
|
+
'active', 'trialing', 'past_due', 'canceled', 'paused',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/** Map account.status → tag-ui variant (status badge in the header).
|
|
52
|
+
* `canceled` carries `tone="muted"` (family-less quiet chrome) instead —
|
|
53
|
+
* `variant="muted"` was never a valid tag-ui value (tag's `variant` enum is
|
|
54
|
+
* the semantic-family axis; `muted` only ever existed on `tone`, the fill-
|
|
55
|
+
* style axis) — a pre-existing bug, fixed while migrating the adjacent
|
|
56
|
+
* accent/muted vocabulary (variant/color/tone convention cut, gh#873/874). */
|
|
57
|
+
const STATUS_TO_VARIANT = {
|
|
58
|
+
active: 'success',
|
|
59
|
+
trialing: 'info',
|
|
60
|
+
past_due: 'danger',
|
|
61
|
+
paused: 'warning',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const STATUS_LABEL = {
|
|
65
|
+
active: 'Active',
|
|
66
|
+
trialing: 'Trialing',
|
|
67
|
+
past_due: 'Past due',
|
|
68
|
+
canceled: 'Canceled',
|
|
69
|
+
paused: 'Paused',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Per-child event → unified `account-action.detail.action` mapping. */
|
|
73
|
+
const DUNNING_INTENT = {
|
|
74
|
+
update: 'update-card',
|
|
75
|
+
retry: 'retry',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Format an amount via Intl.NumberFormat. */
|
|
79
|
+
function formatAmount(amount, currency, locale) {
|
|
80
|
+
const value = Number.isFinite(amount) ? amount : 0;
|
|
81
|
+
try {
|
|
82
|
+
return new Intl.NumberFormat(locale || undefined, {
|
|
83
|
+
style: 'currency',
|
|
84
|
+
currency: currency || 'USD',
|
|
85
|
+
maximumFractionDigits: Number.isInteger(value) ? 0 : 2,
|
|
86
|
+
}).format(value);
|
|
87
|
+
} catch {
|
|
88
|
+
return `${currency || '$'}${value}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Format an ISO date via Intl.DateTimeFormat (medium style). */
|
|
93
|
+
function formatDate(iso, locale) {
|
|
94
|
+
if (!iso) return '';
|
|
95
|
+
try {
|
|
96
|
+
const d = iso instanceof Date ? iso : new Date(iso);
|
|
97
|
+
if (Number.isNaN(d.getTime())) return String(iso);
|
|
98
|
+
return new Intl.DateTimeFormat(locale || undefined, {
|
|
99
|
+
year: 'numeric',
|
|
100
|
+
month: 'short',
|
|
101
|
+
day: '2-digit',
|
|
102
|
+
}).format(d);
|
|
103
|
+
} catch {
|
|
104
|
+
return String(iso);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class UIBillingOverview extends UIElement {
|
|
109
|
+
static get properties() {
|
|
110
|
+
return {
|
|
111
|
+
account: { type: Object, default: null },
|
|
112
|
+
variant: { type: String, default: 'full', reflect: true },
|
|
113
|
+
currency: { type: String, default: 'USD', reflect: true },
|
|
114
|
+
locale: { type: String, default: '' },
|
|
115
|
+
loading: { type: Boolean, default: false, reflect: true },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// No template — composite stamps a fixed region skeleton once
|
|
120
|
+
// and mutates per-region nodes across renders. Returning html``
|
|
121
|
+
// would trigger stamp() → replaceChildren() and wipe authored
|
|
122
|
+
// slotted children + lose internal state inside the child
|
|
123
|
+
// composites (plan-picker, payment-method-list, invoice-history).
|
|
124
|
+
static template = () => null;
|
|
125
|
+
|
|
126
|
+
// ── Field refs ────────────────────────────────────────────────────
|
|
127
|
+
#stamped = false;
|
|
128
|
+
#cleanups = [];
|
|
129
|
+
|
|
130
|
+
// Header region
|
|
131
|
+
#headerHost = null;
|
|
132
|
+
#headerTitle = null;
|
|
133
|
+
#headerSubtitle = null;
|
|
134
|
+
#headerStatus = null;
|
|
135
|
+
#headerActions = null;
|
|
136
|
+
|
|
137
|
+
// KPI strip
|
|
138
|
+
#kpisHost = null;
|
|
139
|
+
#kpiSpend = null;
|
|
140
|
+
#kpiSeats = null;
|
|
141
|
+
#kpiInvoice = null;
|
|
142
|
+
|
|
143
|
+
// Dunning banner
|
|
144
|
+
#dunningHost = null;
|
|
145
|
+
#dunningAlert = null;
|
|
146
|
+
|
|
147
|
+
// Plan-picker section
|
|
148
|
+
#planPickerHost = null;
|
|
149
|
+
#planPickerHeading = null;
|
|
150
|
+
#planPicker = null;
|
|
151
|
+
|
|
152
|
+
// Payment-methods section
|
|
153
|
+
#paymentMethodsHost = null;
|
|
154
|
+
#paymentMethodsHeading = null;
|
|
155
|
+
#paymentMethods = null;
|
|
156
|
+
|
|
157
|
+
// Invoice-history section
|
|
158
|
+
#invoiceHistoryHost = null;
|
|
159
|
+
#invoiceHistoryHeading = null;
|
|
160
|
+
#invoiceHistory = null;
|
|
161
|
+
|
|
162
|
+
// Empty state
|
|
163
|
+
#emptyHost = null;
|
|
164
|
+
|
|
165
|
+
// Bookkeeping
|
|
166
|
+
#headingId = '';
|
|
167
|
+
#warnedStatus = false;
|
|
168
|
+
#loadCount = 0;
|
|
169
|
+
|
|
170
|
+
// ── Public imperative API ─────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/** Re-fetch from `data-stream-src` when configured. */
|
|
173
|
+
async refresh() {
|
|
174
|
+
const src = this.getAttribute('data-stream-src');
|
|
175
|
+
if (!src) return;
|
|
176
|
+
const tag = ++this.#loadCount;
|
|
177
|
+
this.loading = true;
|
|
178
|
+
this.removeAttribute('error');
|
|
179
|
+
try {
|
|
180
|
+
const res = await fetch(src);
|
|
181
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
182
|
+
const data = await res.json();
|
|
183
|
+
if (tag !== this.#loadCount) return;
|
|
184
|
+
this.loading = false;
|
|
185
|
+
const record = data && typeof data === 'object' && data.account
|
|
186
|
+
? data.account
|
|
187
|
+
: data;
|
|
188
|
+
this.account = record;
|
|
189
|
+
this.dispatchEvent(new CustomEvent('load', {
|
|
190
|
+
bubbles: true,
|
|
191
|
+
detail: { account: record },
|
|
192
|
+
}));
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (tag !== this.#loadCount) return;
|
|
195
|
+
this.loading = false;
|
|
196
|
+
this.setAttribute('error', '');
|
|
197
|
+
this.dispatchEvent(new CustomEvent('error', {
|
|
198
|
+
bubbles: true,
|
|
199
|
+
detail: { reason: 'fetch-failed', message: String(err?.message || err) },
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Lifecycle ─────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
connected() {
|
|
207
|
+
this.setAttribute('role', 'region');
|
|
208
|
+
|
|
209
|
+
if (!this.#stamped) {
|
|
210
|
+
this.#absorbInlineAccountScript();
|
|
211
|
+
this.#stamp();
|
|
212
|
+
this.#wireListeners();
|
|
213
|
+
this.#stamped = true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
untracked(() => this.render());
|
|
217
|
+
|
|
218
|
+
if (this.getAttribute('data-stream-src')) {
|
|
219
|
+
this.refresh();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
disconnected() {
|
|
224
|
+
for (const fn of this.#cleanups) {
|
|
225
|
+
try { fn(); } catch { /* ignore stale */ }
|
|
226
|
+
}
|
|
227
|
+
this.#cleanups.length = 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
231
|
+
super.attributeChangedCallback?.(name, oldVal, newVal);
|
|
232
|
+
if (!this.#stamped) return;
|
|
233
|
+
if (name === 'data-stream-src' && oldVal !== newVal && newVal) {
|
|
234
|
+
this.refresh();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Render — mutates per-region nodes only ────────────────────────
|
|
239
|
+
|
|
240
|
+
render() {
|
|
241
|
+
if (!this.#stamped) return;
|
|
242
|
+
|
|
243
|
+
const account = this.account;
|
|
244
|
+
const isEmpty = !account && !this.getAttribute('data-stream-src');
|
|
245
|
+
|
|
246
|
+
if (isEmpty) this.setAttribute('empty', '');
|
|
247
|
+
else this.removeAttribute('empty');
|
|
248
|
+
|
|
249
|
+
if (this.loading) this.setAttribute('aria-busy', 'true');
|
|
250
|
+
else this.removeAttribute('aria-busy');
|
|
251
|
+
|
|
252
|
+
// State attribute reflection (past-due / trial).
|
|
253
|
+
const status = account?.status || '';
|
|
254
|
+
if (status === 'past_due') this.setAttribute('past-due', '');
|
|
255
|
+
else this.removeAttribute('past-due');
|
|
256
|
+
if (status === 'trialing') this.setAttribute('trial', '');
|
|
257
|
+
else this.removeAttribute('trial');
|
|
258
|
+
|
|
259
|
+
if (status && !KNOWN_STATUSES.has(status) && !this.#warnedStatus) {
|
|
260
|
+
this.#warnedStatus = true;
|
|
261
|
+
// eslint-disable-next-line no-console
|
|
262
|
+
console.warn(
|
|
263
|
+
`[billing-overview-ui] unknown account.status "${status}" — rendering default chrome.`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (isEmpty) {
|
|
268
|
+
this.#renderHeader(null);
|
|
269
|
+
this.#renderAccessibleLabel(null);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!account) {
|
|
274
|
+
// loading + no account yet — render header chrome only.
|
|
275
|
+
this.#renderHeader(null);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.#renderHeader(account);
|
|
280
|
+
this.#renderKpis(account);
|
|
281
|
+
this.#renderDunning(account);
|
|
282
|
+
this.#renderPlanPicker(account);
|
|
283
|
+
this.#renderPaymentMethods(account);
|
|
284
|
+
this.#renderInvoiceHistory(account);
|
|
285
|
+
this.#renderAccessibleLabel(account);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Stamp — six-region skeleton ───────────────────────────────────
|
|
289
|
+
|
|
290
|
+
#absorbInlineAccountScript() {
|
|
291
|
+
const script = this.querySelector(
|
|
292
|
+
':scope > script[type="application/json"][data-account]',
|
|
293
|
+
);
|
|
294
|
+
if (!script) return;
|
|
295
|
+
try {
|
|
296
|
+
const parsed = JSON.parse(script.textContent || '{}');
|
|
297
|
+
if (parsed && typeof parsed === 'object') {
|
|
298
|
+
const record = parsed.account && typeof parsed.account === 'object'
|
|
299
|
+
? parsed.account
|
|
300
|
+
: parsed;
|
|
301
|
+
this.account = record;
|
|
302
|
+
}
|
|
303
|
+
} catch (err) {
|
|
304
|
+
// eslint-disable-next-line no-console
|
|
305
|
+
console.warn(
|
|
306
|
+
'[billing-overview-ui] failed to parse <script data-account> JSON:',
|
|
307
|
+
err?.message,
|
|
308
|
+
);
|
|
309
|
+
} finally {
|
|
310
|
+
script.remove();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Move consumer-authored `[slot=...]` direct children into their
|
|
316
|
+
* target regions. Light-DOM substrate (ADR-0033) — `slot=` is
|
|
317
|
+
* decorative metadata; <slot> elements do NOT reparent children
|
|
318
|
+
* the way they do in shadow DOM.
|
|
319
|
+
*/
|
|
320
|
+
#absorbSlottedChildren(slotName, target) {
|
|
321
|
+
const nodes = this.querySelectorAll(`:scope > [slot="${slotName}"]`);
|
|
322
|
+
for (const n of nodes) target.appendChild(n);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#stamp() {
|
|
326
|
+
this.#headingId = `bo-h-${Math.random().toString(36).slice(2, 8)}`;
|
|
327
|
+
|
|
328
|
+
// ── Header region ── card-ui wrapping plan title + status tag + actions.
|
|
329
|
+
// Card chrome matches canonical billing.contents.html plan card pattern
|
|
330
|
+
// (apps/saas/app/billing/billing.contents.html lines 13-29). See
|
|
331
|
+
// .brain/findings/demos/billing-overview.wireframe.md §Level 5 tree.
|
|
332
|
+
const headerCard = document.createElement('card-ui');
|
|
333
|
+
headerCard.setAttribute('data-header', '');
|
|
334
|
+
|
|
335
|
+
const header = document.createElement('header');
|
|
336
|
+
|
|
337
|
+
// Icon slot — avatar-ui per canonical convention (star/credit-card iconography).
|
|
338
|
+
const headerIconSpan = document.createElement('span');
|
|
339
|
+
headerIconSpan.setAttribute('slot', 'icon');
|
|
340
|
+
const headerAvatar = document.createElement('avatar-ui');
|
|
341
|
+
headerAvatar.setAttribute('icon', 'credit-card');
|
|
342
|
+
headerIconSpan.appendChild(headerAvatar);
|
|
343
|
+
|
|
344
|
+
// Heading slot — plan name + status badge.
|
|
345
|
+
const headerHeadingSpan = document.createElement('span');
|
|
346
|
+
headerHeadingSpan.setAttribute('slot', 'heading');
|
|
347
|
+
|
|
348
|
+
const title = document.createElement('h2');
|
|
349
|
+
title.id = this.#headingId;
|
|
350
|
+
title.setAttribute('data-billing-plan-name', '');
|
|
351
|
+
headerHeadingSpan.appendChild(title);
|
|
352
|
+
|
|
353
|
+
const statusBadge = document.createElement('tag-ui');
|
|
354
|
+
statusBadge.setAttribute('data-billing-status', '');
|
|
355
|
+
statusBadge.setAttribute('size', 'md');
|
|
356
|
+
headerHeadingSpan.appendChild(statusBadge);
|
|
357
|
+
|
|
358
|
+
// Description slot — plan price/cycle/next-invoice meta.
|
|
359
|
+
const headerDescriptionSpan = document.createElement('span');
|
|
360
|
+
headerDescriptionSpan.setAttribute('slot', 'description');
|
|
361
|
+
const subtitle = document.createElement('div');
|
|
362
|
+
subtitle.setAttribute('data-billing-plan-meta', '');
|
|
363
|
+
headerDescriptionSpan.appendChild(subtitle);
|
|
364
|
+
|
|
365
|
+
// Action slot — change-plan + consumer-supplied actions.
|
|
366
|
+
const headerActionSpan = document.createElement('span');
|
|
367
|
+
headerActionSpan.setAttribute('slot', 'action');
|
|
368
|
+
const headerActions = document.createElement('div');
|
|
369
|
+
headerActions.setAttribute('data-header-actions', '');
|
|
370
|
+
headerActionSpan.appendChild(headerActions);
|
|
371
|
+
|
|
372
|
+
header.append(headerIconSpan, headerHeadingSpan, headerDescriptionSpan, headerActionSpan);
|
|
373
|
+
headerCard.appendChild(header);
|
|
374
|
+
this.appendChild(headerCard);
|
|
375
|
+
|
|
376
|
+
this.#headerHost = headerCard;
|
|
377
|
+
this.#headerTitle = title;
|
|
378
|
+
this.#headerSubtitle = subtitle;
|
|
379
|
+
this.#headerStatus = statusBadge;
|
|
380
|
+
this.#headerActions = headerActions;
|
|
381
|
+
|
|
382
|
+
// Absorb consumer [slot="header-actions"] children.
|
|
383
|
+
this.#absorbSlottedChildren('header-actions', headerActions);
|
|
384
|
+
|
|
385
|
+
// ── KPI strip ── card-ui × 3 wrapping stat-ui (currentSpend · seats · nextInvoice).
|
|
386
|
+
// Card chrome matches canonical KPI strip pattern from
|
|
387
|
+
// apps/saas/app/admin-dashboard/admin-dashboard.contents.html (card-ui +
|
|
388
|
+
// section + stat-ui). See wireframe Level 5 tree.
|
|
389
|
+
const kpis = document.createElement('section');
|
|
390
|
+
kpis.setAttribute('data-kpis', '');
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Helper: wrap a stat-ui in <card-ui><section> per canonical KPI pattern.
|
|
394
|
+
* @returns {[HTMLElement, HTMLElement]} [cardWrapper, statElement]
|
|
395
|
+
*/
|
|
396
|
+
const makeKpiCard = (kpiId, label, icon) => {
|
|
397
|
+
const card = document.createElement('card-ui');
|
|
398
|
+
card.setAttribute('data-kpi-card', kpiId);
|
|
399
|
+
const section = document.createElement('section');
|
|
400
|
+
const stat = document.createElement('stat-ui');
|
|
401
|
+
stat.setAttribute('data-kpi', kpiId);
|
|
402
|
+
stat.setAttribute('label', label);
|
|
403
|
+
stat.setAttribute('value', '—');
|
|
404
|
+
stat.setAttribute('icon', icon);
|
|
405
|
+
section.appendChild(stat);
|
|
406
|
+
card.appendChild(section);
|
|
407
|
+
return [card, stat];
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const [kpiSpendCard, kpiSpend] = makeKpiCard('spend', 'Current month', 'credit-card');
|
|
411
|
+
const [kpiSeatsCard, kpiSeats] = makeKpiCard('seats', 'Seats', 'users-three');
|
|
412
|
+
const [kpiInvoiceCard, kpiInvoice] = makeKpiCard('next-invoice', 'Next invoice', 'calendar');
|
|
413
|
+
|
|
414
|
+
kpis.append(kpiSpendCard, kpiSeatsCard, kpiInvoiceCard);
|
|
415
|
+
this.appendChild(kpis);
|
|
416
|
+
this.#kpisHost = kpis;
|
|
417
|
+
this.#kpiSpend = kpiSpend;
|
|
418
|
+
this.#kpiSeats = kpiSeats;
|
|
419
|
+
this.#kpiInvoice = kpiInvoice;
|
|
420
|
+
|
|
421
|
+
// ── Dunning banner ── alert-ui[pattern="dunning"], conditional.
|
|
422
|
+
const dunningHost = document.createElement('section');
|
|
423
|
+
dunningHost.setAttribute('data-dunning', '');
|
|
424
|
+
dunningHost.setAttribute('hidden', '');
|
|
425
|
+
const dunningAlert = document.createElement('alert-ui');
|
|
426
|
+
dunningAlert.setAttribute('pattern', 'dunning');
|
|
427
|
+
dunningAlert.setAttribute('variant', 'danger');
|
|
428
|
+
dunningHost.appendChild(dunningAlert);
|
|
429
|
+
this.appendChild(dunningHost);
|
|
430
|
+
this.#dunningHost = dunningHost;
|
|
431
|
+
this.#dunningAlert = dunningAlert;
|
|
432
|
+
|
|
433
|
+
// ── Plan-picker section ── conditional on [variant=full].
|
|
434
|
+
const planPickerHost = document.createElement('section');
|
|
435
|
+
planPickerHost.setAttribute('data-plan-picker', '');
|
|
436
|
+
const planPickerHeading = document.createElement('h3');
|
|
437
|
+
planPickerHeading.setAttribute('data-section-heading', '');
|
|
438
|
+
planPickerHeading.textContent = 'Plan';
|
|
439
|
+
const planPicker = document.createElement('plan-picker-ui');
|
|
440
|
+
planPicker.setAttribute('data-billing-plan-picker', '');
|
|
441
|
+
planPicker.setAttribute('name', 'billing-overview-plan');
|
|
442
|
+
planPickerHost.append(planPickerHeading, planPicker);
|
|
443
|
+
|
|
444
|
+
// Optional before-plan-picker slot anchor sits before the plan picker
|
|
445
|
+
// section card. We absorb consumer-authored content into planPickerHost
|
|
446
|
+
// ahead of the heading.
|
|
447
|
+
this.appendChild(planPickerHost);
|
|
448
|
+
this.#planPickerHost = planPickerHost;
|
|
449
|
+
this.#planPickerHeading = planPickerHeading;
|
|
450
|
+
this.#planPicker = planPicker;
|
|
451
|
+
|
|
452
|
+
// Absorb before-plan-picker slot content into the section
|
|
453
|
+
// ahead of the plan picker.
|
|
454
|
+
const beforePlanPicker = document.createElement('div');
|
|
455
|
+
beforePlanPicker.setAttribute('data-before-plan-picker', '');
|
|
456
|
+
planPickerHost.insertBefore(beforePlanPicker, planPickerHeading);
|
|
457
|
+
this.#absorbSlottedChildren('before-plan-picker', beforePlanPicker);
|
|
458
|
+
|
|
459
|
+
// ── Payment-methods section ──
|
|
460
|
+
const paymentMethodsHost = document.createElement('section');
|
|
461
|
+
paymentMethodsHost.setAttribute('data-payment-methods', '');
|
|
462
|
+
const paymentMethodsHeading = document.createElement('h3');
|
|
463
|
+
paymentMethodsHeading.setAttribute('data-section-heading', '');
|
|
464
|
+
paymentMethodsHeading.textContent = 'Payment methods';
|
|
465
|
+
const paymentMethods = document.createElement('payment-method-list-ui');
|
|
466
|
+
paymentMethods.setAttribute('data-billing-payment-methods', '');
|
|
467
|
+
paymentMethods.setAttribute('name', 'billing-overview-payment-method');
|
|
468
|
+
paymentMethodsHost.append(paymentMethodsHeading, paymentMethods);
|
|
469
|
+
this.appendChild(paymentMethodsHost);
|
|
470
|
+
this.#paymentMethodsHost = paymentMethodsHost;
|
|
471
|
+
this.#paymentMethodsHeading = paymentMethodsHeading;
|
|
472
|
+
this.#paymentMethods = paymentMethods;
|
|
473
|
+
|
|
474
|
+
// ── Invoice-history section ──
|
|
475
|
+
const invoiceHistoryHost = document.createElement('section');
|
|
476
|
+
invoiceHistoryHost.setAttribute('data-invoice-history', '');
|
|
477
|
+
const invoiceHistoryHeading = document.createElement('h3');
|
|
478
|
+
invoiceHistoryHeading.setAttribute('data-section-heading', '');
|
|
479
|
+
invoiceHistoryHeading.textContent = 'Invoice history';
|
|
480
|
+
const invoiceHistory = document.createElement('invoice-history-ui');
|
|
481
|
+
invoiceHistory.setAttribute('data-billing-invoice-history', '');
|
|
482
|
+
invoiceHistoryHost.append(invoiceHistoryHeading, invoiceHistory);
|
|
483
|
+
this.appendChild(invoiceHistoryHost);
|
|
484
|
+
this.#invoiceHistoryHost = invoiceHistoryHost;
|
|
485
|
+
this.#invoiceHistoryHeading = invoiceHistoryHeading;
|
|
486
|
+
this.#invoiceHistory = invoiceHistory;
|
|
487
|
+
|
|
488
|
+
// After-invoice-history slot anchor.
|
|
489
|
+
const afterInvoiceHistory = document.createElement('div');
|
|
490
|
+
afterInvoiceHistory.setAttribute('data-after-invoice-history', '');
|
|
491
|
+
invoiceHistoryHost.appendChild(afterInvoiceHistory);
|
|
492
|
+
this.#absorbSlottedChildren('after-invoice-history', afterInvoiceHistory);
|
|
493
|
+
|
|
494
|
+
// ── Empty state ──
|
|
495
|
+
const empty = document.createElement('div');
|
|
496
|
+
empty.setAttribute('data-empty', '');
|
|
497
|
+
const emptyHeading = document.createElement('p');
|
|
498
|
+
emptyHeading.setAttribute('data-empty-heading', '');
|
|
499
|
+
emptyHeading.textContent = 'No billing account loaded';
|
|
500
|
+
const emptyDescription = document.createElement('p');
|
|
501
|
+
emptyDescription.setAttribute('data-empty-description', '');
|
|
502
|
+
emptyDescription.textContent = 'Pass an `account` snapshot or `data-stream-src` to render the dashboard.';
|
|
503
|
+
empty.append(emptyHeading, emptyDescription);
|
|
504
|
+
this.appendChild(empty);
|
|
505
|
+
this.#emptyHost = empty;
|
|
506
|
+
|
|
507
|
+
// Absorb consumer-authored [slot="empty"] override.
|
|
508
|
+
this.#absorbSlottedChildren('empty', this);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ── Region renderers ──────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
#renderHeader(account) {
|
|
514
|
+
if (!this.#headerTitle || !this.#headerStatus || !this.#headerSubtitle) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const plan = account?.plan || null;
|
|
519
|
+
const name = plan?.name ? String(plan.name) : 'Billing overview';
|
|
520
|
+
if (this.#headerTitle.textContent !== name) {
|
|
521
|
+
this.#headerTitle.textContent = name;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Subtitle — formatted price · cycle.
|
|
525
|
+
const subtitle = this.#headerSubtitle;
|
|
526
|
+
subtitle.replaceChildren();
|
|
527
|
+
if (plan && Number.isFinite(plan.price)) {
|
|
528
|
+
const currency = plan.currency || this.currency || 'USD';
|
|
529
|
+
const locale = this.#resolveLocale();
|
|
530
|
+
const cycle = plan.cycle === 'year' ? '/yr' : '/mo';
|
|
531
|
+
const priceEl = document.createElement('span');
|
|
532
|
+
priceEl.setAttribute('data-plan-price', '');
|
|
533
|
+
priceEl.textContent = `${formatAmount(plan.price, currency, locale)} ${cycle}`;
|
|
534
|
+
subtitle.appendChild(priceEl);
|
|
535
|
+
}
|
|
536
|
+
if (account?.billingCycle && account.billingCycle !== plan?.cycle) {
|
|
537
|
+
const cycleEl = document.createElement('span');
|
|
538
|
+
cycleEl.setAttribute('data-plan-billing-cycle', '');
|
|
539
|
+
cycleEl.textContent = `Billed ${account.billingCycle}ly`;
|
|
540
|
+
subtitle.appendChild(cycleEl);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Status badge — variant + label from STATUS_TO_VARIANT.
|
|
544
|
+
const status = account?.status || '';
|
|
545
|
+
if (!status) {
|
|
546
|
+
this.#headerStatus.setAttribute('hidden', '');
|
|
547
|
+
} else {
|
|
548
|
+
this.#headerStatus.removeAttribute('hidden');
|
|
549
|
+
const variant = STATUS_TO_VARIANT[status];
|
|
550
|
+
const label = STATUS_LABEL[status] || status;
|
|
551
|
+
if (variant) {
|
|
552
|
+
if (this.#headerStatus.getAttribute('variant') !== variant) {
|
|
553
|
+
this.#headerStatus.setAttribute('variant', variant);
|
|
554
|
+
}
|
|
555
|
+
this.#headerStatus.removeAttribute('tone');
|
|
556
|
+
} else {
|
|
557
|
+
this.#headerStatus.removeAttribute('variant');
|
|
558
|
+
if (this.#headerStatus.getAttribute('tone') !== 'muted') {
|
|
559
|
+
this.#headerStatus.setAttribute('tone', 'muted');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (this.#headerStatus.getAttribute('text') !== label) {
|
|
563
|
+
this.#headerStatus.setAttribute('text', label);
|
|
564
|
+
}
|
|
565
|
+
this.#headerStatus.setAttribute('aria-label', `Account status: ${label}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#renderKpis(account) {
|
|
570
|
+
if (!this.#kpiSpend || !this.#kpiSeats || !this.#kpiInvoice) return;
|
|
571
|
+
|
|
572
|
+
const currency = account?.plan?.currency || this.currency || 'USD';
|
|
573
|
+
const locale = this.#resolveLocale();
|
|
574
|
+
|
|
575
|
+
// KPI 1 — current month spend.
|
|
576
|
+
if (Number.isFinite(account?.currentSpend)) {
|
|
577
|
+
this.#kpiSpend.setAttribute(
|
|
578
|
+
'value',
|
|
579
|
+
formatAmount(account.currentSpend, currency, locale),
|
|
580
|
+
);
|
|
581
|
+
} else {
|
|
582
|
+
this.#kpiSpend.setAttribute('value', '—');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// KPI 2 — seats (used / total).
|
|
586
|
+
if (account?.seats && Number.isFinite(account.seats.used)) {
|
|
587
|
+
const used = account.seats.used;
|
|
588
|
+
const total = Number.isFinite(account.seats.total) ? account.seats.total : null;
|
|
589
|
+
this.#kpiSeats.setAttribute(
|
|
590
|
+
'value',
|
|
591
|
+
total != null ? `${used} / ${total}` : String(used),
|
|
592
|
+
);
|
|
593
|
+
} else {
|
|
594
|
+
this.#kpiSeats.setAttribute('value', '—');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// KPI 3 — next invoice date.
|
|
598
|
+
if (account?.nextInvoiceDate) {
|
|
599
|
+
this.#kpiInvoice.setAttribute(
|
|
600
|
+
'value',
|
|
601
|
+
formatDate(account.nextInvoiceDate, locale),
|
|
602
|
+
);
|
|
603
|
+
} else {
|
|
604
|
+
this.#kpiInvoice.setAttribute('value', '—');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Forward [loading] to each stat.
|
|
608
|
+
for (const stat of [this.#kpiSpend, this.#kpiSeats, this.#kpiInvoice]) {
|
|
609
|
+
if (this.loading) stat.setAttribute('loading', '');
|
|
610
|
+
else stat.removeAttribute('loading');
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
#renderDunning(account) {
|
|
615
|
+
if (!this.#dunningHost || !this.#dunningAlert) return;
|
|
616
|
+
|
|
617
|
+
const dunning = account?.dunning || null;
|
|
618
|
+
const isPastDue = account?.status === 'past_due';
|
|
619
|
+
|
|
620
|
+
if (!isPastDue || !dunning) {
|
|
621
|
+
this.#dunningHost.setAttribute('hidden', '');
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
this.#dunningHost.removeAttribute('hidden');
|
|
626
|
+
|
|
627
|
+
const amount = Number.isFinite(dunning.amount) ? dunning.amount : null;
|
|
628
|
+
const currency = dunning.currency || account?.plan?.currency || this.currency || 'USD';
|
|
629
|
+
|
|
630
|
+
if (amount != null) {
|
|
631
|
+
this.#dunningAlert.setAttribute('amount', String(amount));
|
|
632
|
+
} else {
|
|
633
|
+
this.#dunningAlert.removeAttribute('amount');
|
|
634
|
+
}
|
|
635
|
+
this.#dunningAlert.setAttribute('currency', currency);
|
|
636
|
+
if (dunning.dueAt) {
|
|
637
|
+
this.#dunningAlert.setAttribute('due-at', String(dunning.dueAt));
|
|
638
|
+
}
|
|
639
|
+
if (dunning.cardLast4) {
|
|
640
|
+
this.#dunningAlert.setAttribute('card-last4', String(dunning.cardLast4));
|
|
641
|
+
}
|
|
642
|
+
if (dunning.reason) {
|
|
643
|
+
this.#dunningAlert.setAttribute('reason', String(dunning.reason));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Stamp dunning action buttons inside the alert's actions slot when
|
|
647
|
+
// none exist (consumer-overridable: any [slot=actions] descendant
|
|
648
|
+
// in the alert is preserved).
|
|
649
|
+
if (!this.#dunningAlert.querySelector(':scope > [slot="actions"]')) {
|
|
650
|
+
const updateBtn = document.createElement('button-ui');
|
|
651
|
+
updateBtn.setAttribute('slot', 'actions');
|
|
652
|
+
updateBtn.setAttribute('data-dunning-action', 'update');
|
|
653
|
+
updateBtn.setAttribute('variant', 'primary');
|
|
654
|
+
updateBtn.setAttribute('text', 'Update payment method');
|
|
655
|
+
this.#dunningAlert.appendChild(updateBtn);
|
|
656
|
+
|
|
657
|
+
const retryBtn = document.createElement('button-ui');
|
|
658
|
+
retryBtn.setAttribute('slot', 'actions');
|
|
659
|
+
retryBtn.setAttribute('data-dunning-action', 'retry');
|
|
660
|
+
retryBtn.setAttribute('variant', 'outline');
|
|
661
|
+
retryBtn.setAttribute('text', 'Retry charge');
|
|
662
|
+
this.#dunningAlert.appendChild(retryBtn);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
#renderPlanPicker(account) {
|
|
667
|
+
if (!this.#planPickerHost || !this.#planPicker) return;
|
|
668
|
+
|
|
669
|
+
// Hide the section when variant != full OR when no plans were supplied.
|
|
670
|
+
const wants = this.variant === 'full';
|
|
671
|
+
const hasPlans = Array.isArray(account?.plans) && account.plans.length > 0;
|
|
672
|
+
const visible = wants && hasPlans;
|
|
673
|
+
|
|
674
|
+
if (!visible) {
|
|
675
|
+
this.#planPickerHost.setAttribute('hidden', '');
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
this.#planPickerHost.removeAttribute('hidden');
|
|
679
|
+
|
|
680
|
+
// Forward plans + current plan id.
|
|
681
|
+
this.#planPicker.plans = account.plans;
|
|
682
|
+
if (account?.plan?.id) {
|
|
683
|
+
this.#planPicker.setAttribute('current', String(account.plan.id));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (this.loading) this.#planPicker.setAttribute('loading', '');
|
|
687
|
+
else this.#planPicker.removeAttribute('loading');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
#renderPaymentMethods(account) {
|
|
691
|
+
if (!this.#paymentMethods) return;
|
|
692
|
+
|
|
693
|
+
const methods = Array.isArray(account?.paymentMethods)
|
|
694
|
+
? account.paymentMethods
|
|
695
|
+
: [];
|
|
696
|
+
this.#paymentMethods.methods = methods;
|
|
697
|
+
|
|
698
|
+
if (this.loading) this.#paymentMethods.setAttribute('loading', '');
|
|
699
|
+
else this.#paymentMethods.removeAttribute('loading');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
#renderInvoiceHistory(account) {
|
|
703
|
+
if (!this.#invoiceHistory) return;
|
|
704
|
+
|
|
705
|
+
const invoices = Array.isArray(account?.invoices)
|
|
706
|
+
? account.invoices
|
|
707
|
+
: [];
|
|
708
|
+
this.#invoiceHistory.invoices = invoices;
|
|
709
|
+
const currency = account?.plan?.currency || this.currency || 'USD';
|
|
710
|
+
this.#invoiceHistory.setAttribute('currency', currency);
|
|
711
|
+
|
|
712
|
+
if (this.loading) this.#invoiceHistory.setAttribute('loading', '');
|
|
713
|
+
else this.#invoiceHistory.removeAttribute('loading');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
#renderAccessibleLabel(account) {
|
|
717
|
+
if (this.#headingId) {
|
|
718
|
+
this.setAttribute('aria-labelledby', this.#headingId);
|
|
719
|
+
}
|
|
720
|
+
if (!account && !this.hasAttribute('aria-label')) {
|
|
721
|
+
this.setAttribute('aria-label', 'No billing account loaded');
|
|
722
|
+
} else if (account) {
|
|
723
|
+
this.removeAttribute('aria-label');
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
#resolveLocale() {
|
|
728
|
+
if (this.locale) return this.locale;
|
|
729
|
+
if (typeof document !== 'undefined') {
|
|
730
|
+
return document.documentElement.lang || '';
|
|
731
|
+
}
|
|
732
|
+
return '';
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ── Event delegation — unified `account-action` ──────────────────
|
|
736
|
+
|
|
737
|
+
#wireListeners() {
|
|
738
|
+
// Generic [data-account-action] click → emit account-action.
|
|
739
|
+
const onActionClick = (e) => {
|
|
740
|
+
const target = e.target?.closest?.('[data-account-action]');
|
|
741
|
+
if (!target || !this.contains(target)) return;
|
|
742
|
+
const action = target.getAttribute('data-account-action') || '';
|
|
743
|
+
if (!action) return;
|
|
744
|
+
e.stopPropagation();
|
|
745
|
+
this.#emitAccountAction(action, 'header', { target });
|
|
746
|
+
};
|
|
747
|
+
this.addEventListener('press', onActionClick);
|
|
748
|
+
this.addEventListener('click', onActionClick);
|
|
749
|
+
this.#cleanups.push(() => this.removeEventListener('press', onActionClick));
|
|
750
|
+
this.#cleanups.push(() => this.removeEventListener('click', onActionClick));
|
|
751
|
+
|
|
752
|
+
// Dunning alert action → emit account-action.
|
|
753
|
+
const onDunning = (e) => {
|
|
754
|
+
const intent = e.detail?.action || '';
|
|
755
|
+
const action = DUNNING_INTENT[intent] || intent || 'dunning';
|
|
756
|
+
this.#emitAccountAction(action, 'alert-dunning', e.detail);
|
|
757
|
+
};
|
|
758
|
+
this.addEventListener('dunning-action', onDunning);
|
|
759
|
+
this.#cleanups.push(
|
|
760
|
+
() => this.removeEventListener('dunning-action', onDunning),
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
// Plan-picker child events → emit account-action.
|
|
764
|
+
const onPlanChange = (e) => {
|
|
765
|
+
const planId = e.detail?.value ?? e.detail?.plan?.id ?? '';
|
|
766
|
+
this.#emitAccountAction('select-plan', 'plan-picker', { planId, ...e.detail });
|
|
767
|
+
};
|
|
768
|
+
this.addEventListener('plan-change', onPlanChange);
|
|
769
|
+
this.#cleanups.push(
|
|
770
|
+
() => this.removeEventListener('plan-change', onPlanChange),
|
|
771
|
+
);
|
|
772
|
+
// plan-picker-ui fires `change` for value changes too — bridge it
|
|
773
|
+
// when the event originates from the embedded picker.
|
|
774
|
+
const onPlanPickerChange = (e) => {
|
|
775
|
+
if (!this.#planPicker || !this.#planPicker.contains(e.target)
|
|
776
|
+
&& e.target !== this.#planPicker) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
this.#emitAccountAction('select-plan', 'plan-picker', e.detail);
|
|
780
|
+
};
|
|
781
|
+
this.addEventListener('change', onPlanPickerChange);
|
|
782
|
+
this.#cleanups.push(
|
|
783
|
+
() => this.removeEventListener('change', onPlanPickerChange),
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
// Payment-method-list child events.
|
|
787
|
+
const onPaymentAdd = () => {
|
|
788
|
+
this.#emitAccountAction('add-payment-method', 'payment-method-list', null);
|
|
789
|
+
};
|
|
790
|
+
this.addEventListener('add', onPaymentAdd);
|
|
791
|
+
this.#cleanups.push(() => this.removeEventListener('add', onPaymentAdd));
|
|
792
|
+
|
|
793
|
+
const onPaymentRemove = (e) => {
|
|
794
|
+
this.#emitAccountAction('remove-payment-method', 'payment-method-list', e.detail);
|
|
795
|
+
};
|
|
796
|
+
this.addEventListener('remove', onPaymentRemove);
|
|
797
|
+
this.#cleanups.push(() => this.removeEventListener('remove', onPaymentRemove));
|
|
798
|
+
|
|
799
|
+
const onPaymentSelect = (e) => {
|
|
800
|
+
this.#emitAccountAction('set-default-payment-method', 'payment-method-list', e.detail);
|
|
801
|
+
};
|
|
802
|
+
this.addEventListener('select', onPaymentSelect);
|
|
803
|
+
this.#cleanups.push(() => this.removeEventListener('select', onPaymentSelect));
|
|
804
|
+
|
|
805
|
+
// Invoice-history child events.
|
|
806
|
+
const onInvoiceRowClick = (e) => {
|
|
807
|
+
this.#emitAccountAction('view-invoice', 'invoice-history', e.detail);
|
|
808
|
+
};
|
|
809
|
+
this.addEventListener('invoice-row-click', onInvoiceRowClick);
|
|
810
|
+
this.#cleanups.push(
|
|
811
|
+
() => this.removeEventListener('invoice-row-click', onInvoiceRowClick),
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
const onInvoiceExport = (e) => {
|
|
815
|
+
this.#emitAccountAction('download-invoice', 'invoice-history', e.detail);
|
|
816
|
+
};
|
|
817
|
+
this.addEventListener('invoice-export', onInvoiceExport);
|
|
818
|
+
this.#cleanups.push(
|
|
819
|
+
() => this.removeEventListener('invoice-export', onInvoiceExport),
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
const onInvoiceAction = (e) => {
|
|
823
|
+
const action = e.detail?.action || 'invoice';
|
|
824
|
+
this.#emitAccountAction(action, 'invoice-history', e.detail);
|
|
825
|
+
};
|
|
826
|
+
this.addEventListener('invoice-action', onInvoiceAction);
|
|
827
|
+
this.#cleanups.push(
|
|
828
|
+
() => this.removeEventListener('invoice-action', onInvoiceAction),
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
#emitAccountAction(action, source, payload) {
|
|
833
|
+
this.dispatchEvent(new CustomEvent('account-action', {
|
|
834
|
+
bubbles: true,
|
|
835
|
+
detail: { action, source, payload: payload || null },
|
|
836
|
+
}));
|
|
837
|
+
}
|
|
838
|
+
}
|