@formata/limitr-ui 0.1.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/README.md +30 -0
- package/dist/current.d.ts +84 -0
- package/dist/current.d.ts.map +1 -0
- package/dist/current.js +1079 -0
- package/dist/current.js.map +1 -0
- package/dist/element.d.ts +90 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +282 -0
- package/dist/element.js.map +1 -0
- package/dist/mod.d.ts +4 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +19 -0
- package/dist/mod.js.map +1 -0
- package/dist/table.d.ts +42 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +742 -0
- package/dist/table.js.map +1 -0
- package/package.json +42 -0
package/dist/table.js
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 Formata, Inc. All rights reserved.
|
|
3
|
+
//
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// you may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
//
|
|
8
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
//
|
|
10
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
// See the License for the specific language governing permissions and
|
|
14
|
+
// limitations under the License.
|
|
15
|
+
//
|
|
16
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
17
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
18
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
19
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
20
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
21
|
+
};
|
|
22
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
23
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
24
|
+
};
|
|
25
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
26
|
+
import { LimitrElement } from "./element.js";
|
|
27
|
+
import { css, html } from "lit";
|
|
28
|
+
/**
|
|
29
|
+
* Pricing table for a Limitr policy, showing all visible plans for a user
|
|
30
|
+
* to choose from (allows them to pick/switch plans).
|
|
31
|
+
*/
|
|
32
|
+
let LimitrPricingTable = class LimitrPricingTable extends LimitrElement {
|
|
33
|
+
constructor() {
|
|
34
|
+
super(...arguments);
|
|
35
|
+
this.interactive = true;
|
|
36
|
+
this.denyPolicyChanges = false;
|
|
37
|
+
this.theme = 'light';
|
|
38
|
+
this.currentPlan = null;
|
|
39
|
+
this.loading = true;
|
|
40
|
+
this.showCouponModal = false;
|
|
41
|
+
this.selectedPlanForCoupon = '';
|
|
42
|
+
this.couponCode = '';
|
|
43
|
+
this.couponError = '';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Styles.
|
|
47
|
+
*/
|
|
48
|
+
static get styles() {
|
|
49
|
+
return css `
|
|
50
|
+
:host {
|
|
51
|
+
display: block;
|
|
52
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
53
|
+
--limitr-bg-primary: #ffffff;
|
|
54
|
+
--limitr-bg-secondary: #f8f9fa;
|
|
55
|
+
--limitr-bg-hover: #f1f3f5;
|
|
56
|
+
--limitr-text-primary: #000000;
|
|
57
|
+
--limitr-text-secondary: #6c757d;
|
|
58
|
+
--limitr-border: #dee2e6;
|
|
59
|
+
--limitr-accent: #000000;
|
|
60
|
+
--limitr-accent-text: #ffffff;
|
|
61
|
+
--limitr-radius: 12px;
|
|
62
|
+
--limitr-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
63
|
+
--limitr-shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
:host([theme="dark"]) {
|
|
67
|
+
--limitr-bg-primary: #1a1a1a;
|
|
68
|
+
--limitr-bg-secondary: #2d2d2d;
|
|
69
|
+
--limitr-bg-hover: #3a3a3a;
|
|
70
|
+
--limitr-text-primary: #ffffff;
|
|
71
|
+
--limitr-text-secondary: #a0a0a0;
|
|
72
|
+
--limitr-border: #404040;
|
|
73
|
+
--limitr-accent: #ffffff;
|
|
74
|
+
--limitr-accent-text: #000000;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.pricing-container {
|
|
78
|
+
display: grid;
|
|
79
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
80
|
+
gap: 24px;
|
|
81
|
+
padding: 24px;
|
|
82
|
+
max-width: 1200px;
|
|
83
|
+
margin: 0 auto;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.plan-card {
|
|
87
|
+
background: var(--limitr-bg-primary);
|
|
88
|
+
border: 2px solid var(--limitr-border);
|
|
89
|
+
border-radius: var(--limitr-radius);
|
|
90
|
+
padding: 32px 24px;
|
|
91
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
92
|
+
box-shadow: var(--limitr-shadow);
|
|
93
|
+
position: relative;
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.plan-card.interactive:hover {
|
|
99
|
+
transform: translateY(-4px);
|
|
100
|
+
box-shadow: var(--limitr-shadow-hover);
|
|
101
|
+
border-color: var(--limitr-accent);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.plan-card.current {
|
|
105
|
+
border-color: var(--limitr-accent);
|
|
106
|
+
border-width: 2px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.current-badge {
|
|
110
|
+
position: absolute;
|
|
111
|
+
top: 16px;
|
|
112
|
+
right: 16px;
|
|
113
|
+
background: var(--limitr-accent);
|
|
114
|
+
color: var(--limitr-accent-text);
|
|
115
|
+
padding: 4px 12px;
|
|
116
|
+
border-radius: 16px;
|
|
117
|
+
font-size: 12px;
|
|
118
|
+
font-weight: 600;
|
|
119
|
+
text-transform: uppercase;
|
|
120
|
+
letter-spacing: 0.5px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.plan-header {
|
|
124
|
+
margin-bottom: 24px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.plan-name {
|
|
128
|
+
font-size: 24px;
|
|
129
|
+
font-weight: 700;
|
|
130
|
+
color: var(--limitr-text-primary);
|
|
131
|
+
margin: 0 0 8px 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.plan-price {
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: baseline;
|
|
137
|
+
gap: 4px;
|
|
138
|
+
margin: 16px 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.price-prefix {
|
|
142
|
+
font-size: 18px;
|
|
143
|
+
color: var(--limitr-text-secondary);
|
|
144
|
+
font-weight: 500;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.price-amount {
|
|
148
|
+
font-size: 48px;
|
|
149
|
+
font-weight: 800;
|
|
150
|
+
color: var(--limitr-text-primary);
|
|
151
|
+
line-height: 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.price-suffix {
|
|
155
|
+
font-size: 16px;
|
|
156
|
+
color: var(--limitr-text-secondary);
|
|
157
|
+
font-weight: 500;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.plan-features {
|
|
161
|
+
list-style: none;
|
|
162
|
+
padding: 0;
|
|
163
|
+
margin: 0 0 32px 0;
|
|
164
|
+
flex-grow: 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.feature-item {
|
|
168
|
+
padding: 16px 12px;
|
|
169
|
+
border-bottom: 1px solid var(--limitr-border);
|
|
170
|
+
display: flex;
|
|
171
|
+
justify-content: space-between;
|
|
172
|
+
align-items: center;
|
|
173
|
+
gap: 16px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.feature-item:first-child {
|
|
177
|
+
padding-top: 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.feature-item:last-child {
|
|
181
|
+
border-bottom: none;
|
|
182
|
+
padding-bottom: 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.feature-name {
|
|
186
|
+
font-size: 15px;
|
|
187
|
+
color: var(--limitr-text-primary);
|
|
188
|
+
font-weight: 500;
|
|
189
|
+
flex: 1;
|
|
190
|
+
line-height: 1.5;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.feature-limit {
|
|
194
|
+
font-size: 14px;
|
|
195
|
+
color: var(--limitr-text-secondary);
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
white-space: nowrap;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.select-button {
|
|
201
|
+
width: 100%;
|
|
202
|
+
padding: 16px;
|
|
203
|
+
border: 2px solid var(--limitr-accent);
|
|
204
|
+
background: transparent;
|
|
205
|
+
color: var(--limitr-accent);
|
|
206
|
+
font-size: 16px;
|
|
207
|
+
font-weight: 600;
|
|
208
|
+
border-radius: 8px;
|
|
209
|
+
cursor: pointer;
|
|
210
|
+
transition: all 0.2s ease;
|
|
211
|
+
text-transform: uppercase;
|
|
212
|
+
letter-spacing: 0.5px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.select-button:hover {
|
|
216
|
+
background: var(--limitr-accent);
|
|
217
|
+
color: var(--limitr-accent-text);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.select-button.current {
|
|
221
|
+
background: var(--limitr-accent);
|
|
222
|
+
color: var(--limitr-accent-text);
|
|
223
|
+
cursor: default;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.select-button:disabled {
|
|
227
|
+
opacity: 0.5;
|
|
228
|
+
cursor: not-allowed;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.loading {
|
|
232
|
+
text-align: center;
|
|
233
|
+
padding: 48px;
|
|
234
|
+
color: var(--limitr-text-secondary);
|
|
235
|
+
font-size: 16px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.empty {
|
|
239
|
+
text-align: center;
|
|
240
|
+
padding: 48px;
|
|
241
|
+
color: var(--limitr-text-secondary);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.empty-title {
|
|
245
|
+
font-size: 20px;
|
|
246
|
+
font-weight: 600;
|
|
247
|
+
margin-bottom: 8px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.coupon-modal-overlay {
|
|
251
|
+
position: fixed;
|
|
252
|
+
top: 0;
|
|
253
|
+
left: 0;
|
|
254
|
+
right: 0;
|
|
255
|
+
bottom: 0;
|
|
256
|
+
background: rgba(0, 0, 0, 0.5);
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
justify-content: center;
|
|
260
|
+
z-index: 2000;
|
|
261
|
+
padding: 24px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.coupon-modal {
|
|
265
|
+
background: var(--limitr-bg-primary);
|
|
266
|
+
border-radius: var(--limitr-radius);
|
|
267
|
+
max-width: 480px;
|
|
268
|
+
width: 100%;
|
|
269
|
+
padding: 32px;
|
|
270
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.coupon-modal-title {
|
|
274
|
+
font-size: 24px;
|
|
275
|
+
font-weight: 700;
|
|
276
|
+
color: var(--limitr-text-primary);
|
|
277
|
+
margin: 0 0 8px 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.coupon-modal-description {
|
|
281
|
+
font-size: 14px;
|
|
282
|
+
color: var(--limitr-text-secondary);
|
|
283
|
+
margin-bottom: 24px;
|
|
284
|
+
line-height: 1.5;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.coupon-input-wrapper {
|
|
288
|
+
margin-bottom: 24px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.coupon-input-label {
|
|
292
|
+
display: block;
|
|
293
|
+
font-size: 14px;
|
|
294
|
+
font-weight: 600;
|
|
295
|
+
color: var(--limitr-text-primary);
|
|
296
|
+
margin-bottom: 8px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.coupon-input {
|
|
300
|
+
width: 100%;
|
|
301
|
+
padding: 12px 16px;
|
|
302
|
+
border: 2px solid var(--limitr-border);
|
|
303
|
+
border-radius: 8px;
|
|
304
|
+
font-size: 16px;
|
|
305
|
+
background: var(--limitr-bg-primary);
|
|
306
|
+
color: var(--limitr-text-primary);
|
|
307
|
+
font-family: inherit;
|
|
308
|
+
transition: border-color 0.2s ease;
|
|
309
|
+
box-sizing: border-box;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.coupon-input:focus {
|
|
313
|
+
outline: none;
|
|
314
|
+
border-color: var(--limitr-accent);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.coupon-input.error {
|
|
318
|
+
border-color: #ef4444;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.coupon-error {
|
|
322
|
+
color: #ef4444;
|
|
323
|
+
font-size: 13px;
|
|
324
|
+
margin-top: 6px;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.coupon-modal-actions {
|
|
328
|
+
display: flex;
|
|
329
|
+
gap: 12px;
|
|
330
|
+
flex-wrap: wrap;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.coupon-button {
|
|
334
|
+
flex: 1;
|
|
335
|
+
min-width: 120px;
|
|
336
|
+
padding: 12px 24px;
|
|
337
|
+
border: 2px solid var(--limitr-accent);
|
|
338
|
+
border-radius: 8px;
|
|
339
|
+
font-size: 14px;
|
|
340
|
+
font-weight: 600;
|
|
341
|
+
cursor: pointer;
|
|
342
|
+
transition: all 0.2s ease;
|
|
343
|
+
text-transform: uppercase;
|
|
344
|
+
letter-spacing: 0.5px;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.coupon-button-primary {
|
|
348
|
+
background: var(--limitr-accent);
|
|
349
|
+
color: var(--limitr-accent-text);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.coupon-button-primary:hover {
|
|
353
|
+
opacity: 0.9;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.coupon-button-secondary {
|
|
357
|
+
background: transparent;
|
|
358
|
+
color: var(--limitr-accent);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.coupon-button-secondary:hover {
|
|
362
|
+
background: var(--limitr-bg-secondary);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.coupon-button-tertiary {
|
|
366
|
+
background: transparent;
|
|
367
|
+
border-color: var(--limitr-border);
|
|
368
|
+
color: var(--limitr-text-secondary);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.coupon-button-tertiary:hover {
|
|
372
|
+
background: var(--limitr-bg-secondary);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
@media (max-width: 768px) {
|
|
376
|
+
.pricing-container {
|
|
377
|
+
grid-template-columns: 1fr;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.coupon-modal-actions {
|
|
381
|
+
flex-direction: column;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.coupon-button {
|
|
385
|
+
width: 100%;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
390
|
+
async connectedCallback() {
|
|
391
|
+
super.connectedCallback();
|
|
392
|
+
await this.loadCurrentPlan();
|
|
393
|
+
}
|
|
394
|
+
async updated(changedProperties) {
|
|
395
|
+
await super.updated(changedProperties);
|
|
396
|
+
if (changedProperties.has('policy') || changedProperties.has('customerId')) {
|
|
397
|
+
await this.loadCurrentPlan();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async loadCurrentPlan() {
|
|
401
|
+
this.loading = true;
|
|
402
|
+
try {
|
|
403
|
+
this.currentPlan = await this.getCustomerPlan();
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
console.error('Error loading customer plan:', e);
|
|
407
|
+
this.currentPlan = null;
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
this.loading = false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async handlePlanSelect(planName) {
|
|
414
|
+
if (!this.interactive)
|
|
415
|
+
return;
|
|
416
|
+
// Get the selected plan details
|
|
417
|
+
const selectedPlan = this.getPlan(planName);
|
|
418
|
+
const hasPaidPrice = selectedPlan?.price && selectedPlan.price.amount > 0;
|
|
419
|
+
// Check if plan allows coupons and show coupon modal if so
|
|
420
|
+
const allowsCoupons = selectedPlan?.price?.allowCoupons === true;
|
|
421
|
+
if (hasPaidPrice && allowsCoupons) {
|
|
422
|
+
// Show coupon modal before proceeding
|
|
423
|
+
this.selectedPlanForCoupon = planName;
|
|
424
|
+
this.couponCode = '';
|
|
425
|
+
this.couponError = '';
|
|
426
|
+
this.showCouponModal = true;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
// Proceed with plan selection (no coupon)
|
|
430
|
+
await this.completePlanSelection(planName, '');
|
|
431
|
+
}
|
|
432
|
+
async completePlanSelection(planName, couponCode) {
|
|
433
|
+
// Get the selected plan details
|
|
434
|
+
const selectedPlan = this.getPlan(planName);
|
|
435
|
+
const hasPaidPrice = selectedPlan?.price && selectedPlan.price.amount > 0;
|
|
436
|
+
// Check if customer has payment method on file
|
|
437
|
+
const customer = await this.customer();
|
|
438
|
+
const hasPaymentMethod = customer?.metadata?.stripe_payment_method_type;
|
|
439
|
+
// If selecting a paid plan without payment method, redirect to Stripe portal
|
|
440
|
+
if (hasPaidPrice && !hasPaymentMethod && this.stripePortalUrl) {
|
|
441
|
+
if (confirm("This plan requires a payment method. You'll be redirected to add your payment details.")) {
|
|
442
|
+
globalThis.location.href = this.stripePortalUrl;
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
// Set coupon code in customer metadata if provided
|
|
447
|
+
if (couponCode && this.policy) {
|
|
448
|
+
customer.metadata = {
|
|
449
|
+
...customer.metadata,
|
|
450
|
+
stripe_coupon_code: couponCode,
|
|
451
|
+
stripe_coupon_status: 'pending',
|
|
452
|
+
};
|
|
453
|
+
// Send customer update with coupon to server
|
|
454
|
+
await this.policy.setCustomer(customer);
|
|
455
|
+
}
|
|
456
|
+
if (!this.denyPolicyChanges && this.policy && confirm("Are you sure you'd like to change plans? This may result in additional charges to your account.")) {
|
|
457
|
+
// if the new plan was not set on the policy, return without emitting an event
|
|
458
|
+
// overwrite_meters is false so that current period usage is kept in-tact
|
|
459
|
+
// any new meters created will have a starting date equal to the latest active meter starting date from prior plan
|
|
460
|
+
if (!await this.policy.setCustomerPlan(this.customerId, planName, false))
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const event = new CustomEvent('plan-select', {
|
|
464
|
+
detail: { planName, couponCode },
|
|
465
|
+
bubbles: true,
|
|
466
|
+
composed: true,
|
|
467
|
+
});
|
|
468
|
+
this.dispatchEvent(event);
|
|
469
|
+
await this.loadCurrentPlan();
|
|
470
|
+
this.requestUpdate();
|
|
471
|
+
}
|
|
472
|
+
async handleCouponSubmit() {
|
|
473
|
+
// Basic validation
|
|
474
|
+
if (!this.couponCode || this.couponCode.trim() === '' || !this.customerId) {
|
|
475
|
+
this.couponError = 'Please enter a coupon code';
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
// Make sure the coupon is valid before applying it
|
|
479
|
+
const coupon = this.couponCode.trim();
|
|
480
|
+
const response = await fetch(`https://api.limitr.dev/v1/stripe-ui/coupon-check?coupon=${coupon}&customerId=${this.customerId}`);
|
|
481
|
+
if (response.ok) {
|
|
482
|
+
const json = await response.json();
|
|
483
|
+
if (json.valid) {
|
|
484
|
+
this.showCouponModal = false;
|
|
485
|
+
this.completePlanSelection(this.selectedPlanForCoupon, this.couponCode.trim());
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
this.couponError = 'Not a valid coupon code';
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
this.couponError = 'Not a valid coupon code';
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
handleCouponSkip() {
|
|
498
|
+
// Close modal and proceed without coupon
|
|
499
|
+
this.showCouponModal = false;
|
|
500
|
+
this.completePlanSelection(this.selectedPlanForCoupon, '');
|
|
501
|
+
}
|
|
502
|
+
handleCouponCancel() {
|
|
503
|
+
// Close modal and don't change plans
|
|
504
|
+
this.showCouponModal = false;
|
|
505
|
+
this.selectedPlanForCoupon = '';
|
|
506
|
+
this.couponCode = '';
|
|
507
|
+
this.couponError = '';
|
|
508
|
+
}
|
|
509
|
+
//deno-lint-ignore no-explicit-any
|
|
510
|
+
formatPrice(price) {
|
|
511
|
+
if (!price)
|
|
512
|
+
return null;
|
|
513
|
+
const amount = typeof price.amount === 'number'
|
|
514
|
+
? price.amount.toFixed(2)
|
|
515
|
+
: price.amount;
|
|
516
|
+
return {
|
|
517
|
+
prefix: price.prefix || '',
|
|
518
|
+
amount,
|
|
519
|
+
suffix: price.suffix || '',
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Pluralize a unit name if needed based on the value.
|
|
524
|
+
*/
|
|
525
|
+
pluralizeUnit(unit, value) {
|
|
526
|
+
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
|
527
|
+
// Don't pluralize if value is 1
|
|
528
|
+
if (numValue === 1)
|
|
529
|
+
return unit;
|
|
530
|
+
// Units that don't need pluralization
|
|
531
|
+
const nonPluralUnits = [
|
|
532
|
+
'GB', 'MB', 'KB', 'TB', 'MiB', 'GiB', 'KiB', 'TiB',
|
|
533
|
+
'ms', 'seconds', 'minutes', 'hours', 'days'
|
|
534
|
+
];
|
|
535
|
+
if (nonPluralUnits.includes(unit)) {
|
|
536
|
+
return unit;
|
|
537
|
+
}
|
|
538
|
+
// Common irregular plurals
|
|
539
|
+
const irregularPlurals = {
|
|
540
|
+
'seat': 'seats',
|
|
541
|
+
'token': 'tokens',
|
|
542
|
+
'request': 'requests',
|
|
543
|
+
'call': 'calls',
|
|
544
|
+
'query': 'queries',
|
|
545
|
+
'credit': 'credits',
|
|
546
|
+
'user': 'users',
|
|
547
|
+
'member': 'members',
|
|
548
|
+
'item': 'items'
|
|
549
|
+
};
|
|
550
|
+
if (irregularPlurals[unit.toLowerCase()]) {
|
|
551
|
+
return irregularPlurals[unit.toLowerCase()];
|
|
552
|
+
}
|
|
553
|
+
// Default: add 's' for pluralization
|
|
554
|
+
if (!unit.endsWith('s')) {
|
|
555
|
+
return unit + 's';
|
|
556
|
+
}
|
|
557
|
+
return unit;
|
|
558
|
+
}
|
|
559
|
+
//deno-lint-ignore no-explicit-any
|
|
560
|
+
formatLimitValue(limit, creditName) {
|
|
561
|
+
if (!limit || limit.value === undefined)
|
|
562
|
+
return 'Unlimited';
|
|
563
|
+
const credit = this.getCredit(creditName);
|
|
564
|
+
const value = limit.value;
|
|
565
|
+
if (credit && credit.stof_units && credit.stof_units !== 'float' && credit.stof_units !== 'int') {
|
|
566
|
+
return `${value} ${credit.stof_units}`;
|
|
567
|
+
}
|
|
568
|
+
if (credit && credit.unit) {
|
|
569
|
+
const pluralizedUnit = this.pluralizeUnit(credit.unit, value);
|
|
570
|
+
return `${value} ${pluralizedUnit}`;
|
|
571
|
+
}
|
|
572
|
+
return `${value}`;
|
|
573
|
+
}
|
|
574
|
+
//deno-lint-ignore no-explicit-any
|
|
575
|
+
renderPlanCard(plan, isCurrent) {
|
|
576
|
+
const price = this.formatPrice(plan.price);
|
|
577
|
+
const entitlements = plan.entitlements || {};
|
|
578
|
+
// Filter out hidden entitlements
|
|
579
|
+
//deno-lint-ignore no-explicit-any
|
|
580
|
+
const visibleEntitlements = Object.entries(entitlements).filter(([_, ent]) => !ent.hidden);
|
|
581
|
+
return html `
|
|
582
|
+
<div class="plan-card ${this.interactive ? 'interactive' : ''} ${isCurrent ? 'current' : ''}">
|
|
583
|
+
${isCurrent ? html `<div class="current-badge">Current Plan</div>` : ''}
|
|
584
|
+
|
|
585
|
+
<div class="plan-header">
|
|
586
|
+
<h3 class="plan-name">${plan.label || plan.name}</h3>
|
|
587
|
+
|
|
588
|
+
${price ? html `
|
|
589
|
+
<div class="plan-price">
|
|
590
|
+
${price.prefix ? html `<span class="price-prefix">${price.prefix}</span>` : ''}
|
|
591
|
+
<span class="price-amount">${price.amount}</span>
|
|
592
|
+
${price.suffix ? html `<span class="price-suffix">${price.suffix}</span>` : ''}
|
|
593
|
+
</div>
|
|
594
|
+
` : ''}
|
|
595
|
+
</div>
|
|
596
|
+
|
|
597
|
+
<ul class="plan-features">
|
|
598
|
+
${
|
|
599
|
+
//deno-lint-ignore no-explicit-any
|
|
600
|
+
visibleEntitlements.map(([entName, entitlement]) => {
|
|
601
|
+
const limit = entitlement.limit;
|
|
602
|
+
return html `
|
|
603
|
+
<li class="feature-item">
|
|
604
|
+
<span class="feature-name">${entitlement.description || entName}</span>
|
|
605
|
+
${limit ? html `
|
|
606
|
+
<span class="feature-limit">${this.formatLimitValue(limit, limit.credit)}</span>
|
|
607
|
+
` : html `
|
|
608
|
+
<span class="feature-limit">✓</span>
|
|
609
|
+
`}
|
|
610
|
+
</li>
|
|
611
|
+
`;
|
|
612
|
+
})}
|
|
613
|
+
</ul>
|
|
614
|
+
|
|
615
|
+
${this.interactive ? html `
|
|
616
|
+
<button
|
|
617
|
+
class="select-button ${isCurrent ? 'current' : ''}"
|
|
618
|
+
?disabled=${isCurrent}
|
|
619
|
+
@click=${() => this.handlePlanSelect(plan.name)}
|
|
620
|
+
>
|
|
621
|
+
${isCurrent ? 'Current Plan' : 'Select Plan'}
|
|
622
|
+
</button>
|
|
623
|
+
` : ''}
|
|
624
|
+
</div>
|
|
625
|
+
`;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Render.
|
|
629
|
+
*/
|
|
630
|
+
render() {
|
|
631
|
+
if (this.loading) {
|
|
632
|
+
return html `<div class="loading">Loading plans...</div>`;
|
|
633
|
+
}
|
|
634
|
+
const plans = this.visiblePlans;
|
|
635
|
+
if (plans.length === 0) {
|
|
636
|
+
return html `
|
|
637
|
+
<div class="empty">
|
|
638
|
+
<div class="empty-title">No plans available</div>
|
|
639
|
+
</div>
|
|
640
|
+
`;
|
|
641
|
+
}
|
|
642
|
+
return html `
|
|
643
|
+
<div class="pricing-container">
|
|
644
|
+
${plans.map(plan => this.renderPlanCard(plan, this.currentPlan?.name === plan.name))}
|
|
645
|
+
</div>
|
|
646
|
+
|
|
647
|
+
${this.showCouponModal ? html `
|
|
648
|
+
<div class="coupon-modal-overlay" @click=${this.handleCouponCancel}>
|
|
649
|
+
<div class="coupon-modal" @click=${(e) => e.stopPropagation()}>
|
|
650
|
+
<h2 class="coupon-modal-title">Have a Coupon Code?</h2>
|
|
651
|
+
<p class="coupon-modal-description">
|
|
652
|
+
Enter your coupon code to apply a discount to your subscription.
|
|
653
|
+
</p>
|
|
654
|
+
|
|
655
|
+
<div class="coupon-input-wrapper">
|
|
656
|
+
<label class="coupon-input-label" for="coupon-input">
|
|
657
|
+
Coupon Code
|
|
658
|
+
</label>
|
|
659
|
+
<input
|
|
660
|
+
id="coupon-input"
|
|
661
|
+
type="text"
|
|
662
|
+
class="coupon-input ${this.couponError ? 'error' : ''}"
|
|
663
|
+
.value=${this.couponCode}
|
|
664
|
+
@input=${(e) => {
|
|
665
|
+
this.couponCode = e.target.value;
|
|
666
|
+
this.couponError = '';
|
|
667
|
+
}}
|
|
668
|
+
@keypress=${(e) => {
|
|
669
|
+
if (e.key === 'Enter') {
|
|
670
|
+
this.handleCouponSubmit();
|
|
671
|
+
}
|
|
672
|
+
}}
|
|
673
|
+
placeholder="Enter code"
|
|
674
|
+
autocomplete="off"
|
|
675
|
+
/>
|
|
676
|
+
${this.couponError ? html `
|
|
677
|
+
<div class="coupon-error">${this.couponError}</div>
|
|
678
|
+
` : ''}
|
|
679
|
+
</div>
|
|
680
|
+
|
|
681
|
+
<div class="coupon-modal-actions">
|
|
682
|
+
<button class="coupon-button coupon-button-primary" @click=${this.handleCouponSubmit}>
|
|
683
|
+
Apply Coupon
|
|
684
|
+
</button>
|
|
685
|
+
<button class="coupon-button coupon-button-secondary" @click=${this.handleCouponSkip}>
|
|
686
|
+
Skip
|
|
687
|
+
</button>
|
|
688
|
+
<button class="coupon-button coupon-button-tertiary" @click=${this.handleCouponCancel}>
|
|
689
|
+
Cancel
|
|
690
|
+
</button>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
` : ''}
|
|
695
|
+
`;
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
__decorate([
|
|
699
|
+
property({ type: Boolean }),
|
|
700
|
+
__metadata("design:type", Boolean)
|
|
701
|
+
], LimitrPricingTable.prototype, "interactive", void 0);
|
|
702
|
+
__decorate([
|
|
703
|
+
property({ type: Boolean })
|
|
704
|
+
/** When true, emit plan select events only, without setting customer plan on policy. */
|
|
705
|
+
,
|
|
706
|
+
__metadata("design:type", Boolean)
|
|
707
|
+
], LimitrPricingTable.prototype, "denyPolicyChanges", void 0);
|
|
708
|
+
__decorate([
|
|
709
|
+
property(),
|
|
710
|
+
__metadata("design:type", String)
|
|
711
|
+
], LimitrPricingTable.prototype, "theme", void 0);
|
|
712
|
+
__decorate([
|
|
713
|
+
state()
|
|
714
|
+
//deno-lint-ignore no-explicit-any
|
|
715
|
+
,
|
|
716
|
+
__metadata("design:type", Object)
|
|
717
|
+
], LimitrPricingTable.prototype, "currentPlan", void 0);
|
|
718
|
+
__decorate([
|
|
719
|
+
state(),
|
|
720
|
+
__metadata("design:type", Boolean)
|
|
721
|
+
], LimitrPricingTable.prototype, "loading", void 0);
|
|
722
|
+
__decorate([
|
|
723
|
+
state(),
|
|
724
|
+
__metadata("design:type", Boolean)
|
|
725
|
+
], LimitrPricingTable.prototype, "showCouponModal", void 0);
|
|
726
|
+
__decorate([
|
|
727
|
+
state(),
|
|
728
|
+
__metadata("design:type", String)
|
|
729
|
+
], LimitrPricingTable.prototype, "selectedPlanForCoupon", void 0);
|
|
730
|
+
__decorate([
|
|
731
|
+
state(),
|
|
732
|
+
__metadata("design:type", String)
|
|
733
|
+
], LimitrPricingTable.prototype, "couponCode", void 0);
|
|
734
|
+
__decorate([
|
|
735
|
+
state(),
|
|
736
|
+
__metadata("design:type", String)
|
|
737
|
+
], LimitrPricingTable.prototype, "couponError", void 0);
|
|
738
|
+
LimitrPricingTable = __decorate([
|
|
739
|
+
customElement('limitr-pricing-table')
|
|
740
|
+
], LimitrPricingTable);
|
|
741
|
+
export { LimitrPricingTable };
|
|
742
|
+
//# sourceMappingURL=table.js.map
|