@getspot/spot-widget 1.4.0 → 2.0.1

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.js DELETED
@@ -1,422 +0,0 @@
1
- import { fetchQuote, fetchMultipleQuotes } from "./api.js";
2
- import { validateOptions } from "./validateOptions.js";
3
- import {
4
- renderHeader,
5
- renderBenefits,
6
- renderQualifyingReasons,
7
- renderCoveredItems,
8
- renderTable,
9
- renderOptions,
10
- renderFooter,
11
- renderPaymentTerms,
12
- makeEl,
13
- } from "./ui.js";
14
-
15
- import styles from "./styles.css?inline";
16
-
17
- function injectStyles(css) {
18
- const styleTag = document.createElement("style");
19
- styleTag.textContent = css;
20
- document.head.appendChild(styleTag);
21
- }
22
-
23
- injectStyles(styles);
24
-
25
- const apiEndpoint = {
26
- sandbox: "https://api.sandbox.getspot.com/api/v1/quote",
27
- production: "https://api.getspot.com/api/v1/quote",
28
- local: "http://localhost:3999/api/v1/quote",
29
- };
30
-
31
- class SpotWidget {
32
- constructor(options = {}) {
33
- this.options = {
34
- location: "body",
35
- showTable: true,
36
- optInSelected: false,
37
- apiConfig: { environment: "production", partnerId: "" },
38
- quoteRequestData: {},
39
- callbacks: {},
40
- ...options,
41
- };
42
-
43
- this._onResize = this._updateLayout.bind(this);
44
-
45
- this.root =
46
- typeof this.options.location === "string"
47
- ? document.querySelector(this.options.location)
48
- : this.options.location;
49
- this.currentSelection = this.options.optInSelected ? "yes" : null;
50
- this._init();
51
- }
52
-
53
- async _init() {
54
- try {
55
- validateOptions(this.options);
56
-
57
- const { environment, partnerId } = this.options.apiConfig;
58
- const customEndpoint = this.options.apiConfig.customEndpoint;
59
-
60
- const endpoint = customEndpoint || apiEndpoint[environment];
61
-
62
- const isBatchQuote =
63
- this.options.quoteRequestData.cartInfo &&
64
- this.options.quoteRequestData.items;
65
- const response = isBatchQuote
66
- ? await fetchMultipleQuotes(
67
- endpoint,
68
- partnerId,
69
- this.options.quoteRequestData
70
- )
71
- : await fetchQuote(endpoint, partnerId, this.options.quoteRequestData);
72
-
73
- if (response.status !== "QUOTE_AVAILABLE") {
74
- if (
75
- response.status === "NO_MATCHING_QUOTE" &&
76
- this.options.callbacks?.noMatchingQuote
77
- ) {
78
- this.options.callbacks.noMatchingQuote({
79
- status: "NO_MATCHING_QUOTE",
80
- data: this.options.quoteRequestData,
81
- });
82
- }
83
- return;
84
- }
85
-
86
- this.quote = response.data;
87
- this._renderWidget();
88
-
89
- if (this.options.optInSelected && this.options.callbacks?.onOptIn) {
90
- const optInData = {
91
- status: "QUOTE_ACCEPTED",
92
- spotPrice: this.quote.spotPrice,
93
- quoteId: this.quote.id,
94
- };
95
-
96
- // For batch quotes, include detailed quote information
97
- if (this.quote.originalQuotes && this.quote.originalQuotes.length > 0) {
98
- optInData.batchQuoteDetails = this.quote.originalQuotes.map((q) => {
99
- const originalItem = this.options.quoteRequestData.items?.find(
100
- (item) =>
101
- (item.cartItemId ||
102
- `item-${
103
- this.options.quoteRequestData.items.indexOf(item) + 1
104
- }`) === q.cartItemId
105
- );
106
- return {
107
- quoteId: q.id,
108
- productPrice: originalItem?.productPrice || q.spotPrice,
109
- cartItemId: q.cartItemId,
110
- };
111
- });
112
- }
113
-
114
- this.options.callbacks.onOptIn(optInData);
115
- }
116
-
117
- if (this.options.callbacks?.onQuoteRetrieved) {
118
- this.options.callbacks.onQuoteRetrieved(this.quote);
119
- }
120
- } catch (err) {
121
- if (this.options.callbacks?.onError) {
122
- this.options.callbacks?.onError({
123
- message: err.message,
124
- status: err.status,
125
- responseBody: err.responseBody,
126
- });
127
- }
128
- }
129
- }
130
-
131
- _renderWidget() {
132
- this.container = document.createElement("div");
133
- this.container.className = "spot-refund-guarantee";
134
- this.root.appendChild(this.container);
135
-
136
- Object.entries(this.options.theme || {}).forEach(([k, v]) => {
137
- const cssVariable = `--${k}`;
138
- this.container.style.setProperty(cssVariable, v);
139
- });
140
-
141
- renderHeader(this.container, this.quote.communication);
142
- const cw = document.createElement("div");
143
- cw.className = "spot-content__wrapper";
144
- this.container.appendChild(cw);
145
-
146
- // Render qualifying reasons if they exist, otherwise render bullet points
147
- if (this.quote.qualifyingReasons) {
148
- renderQualifyingReasons(cw, this.quote.qualifyingReasons);
149
- } else {
150
- renderBenefits(cw, this.quote.communication.bulletPoints);
151
- }
152
-
153
- if (this.quote.coveredItems) {
154
- renderCoveredItems(cw, this.quote.coveredItems);
155
- }
156
-
157
- // Only show table if showTable is true and qualifyingReasons don't exist (pass/trip offers don't have payout schedules)
158
- if (this.options.showTable && !this.quote.qualifyingReasons) {
159
- renderTable(cw, this.quote.payoutSchedule);
160
- }
161
- const optsEl = renderOptions(
162
- cw,
163
- this.options.optInSelected,
164
- this.quote.communication
165
- );
166
- cw.appendChild(optsEl);
167
- this.paymentTermsEl = makeEl("div", {
168
- className: "spot-payment-terms__wrapper",
169
- parent: cw,
170
- });
171
- renderFooter(this.container, this.quote);
172
-
173
- window.addEventListener("resize", this._onResize);
174
- this._updateLayout();
175
- this._setupOptionListeners(optsEl);
176
- }
177
-
178
- _updateLayout() {
179
- const isDesktop = window.matchMedia("(min-width: 768px)").matches;
180
- const hasTable = this.options.showTable && !this.quote.qualifyingReasons;
181
- this.container
182
- .querySelector(".spot-content__wrapper")
183
- .classList.toggle("desktop-layout", isDesktop && hasTable);
184
- }
185
-
186
- _setupOptionListeners(el) {
187
- const radioButtons = el.querySelectorAll('input[type="radio"]');
188
- const options = el.querySelectorAll(".spot-selection__option");
189
-
190
- radioButtons.forEach((radio) => {
191
- radio.addEventListener("change", (e) => {
192
- const val = e.target.value;
193
- this.hideSelectionError();
194
-
195
- this.currentSelection = val;
196
-
197
- options.forEach((label) => label.classList.remove("selected"));
198
- e.target.closest(".spot-selection__option")?.classList.add("selected");
199
-
200
- if (this.paymentTermsEl) this.paymentTermsEl.innerHTML = "";
201
-
202
- if (val === "yes") {
203
- if (this.options.quoteRequestData.isPartialPayment) {
204
- renderPaymentTerms(this.paymentTermsEl, this.quote);
205
- }
206
- if (this.options.callbacks?.onOptIn) {
207
- const optInData = {
208
- status: "QUOTE_ACCEPTED",
209
- spotPrice: this.quote.spotPrice,
210
- quoteId: this.quote.id,
211
- };
212
-
213
- // For batch quotes, include detailed quote information
214
- if (
215
- this.quote.originalQuotes &&
216
- this.quote.originalQuotes.length > 0
217
- ) {
218
- optInData.batchQuoteDetails = this.quote.originalQuotes.map(
219
- (q) => {
220
- const originalItem =
221
- this.options.quoteRequestData.items?.find(
222
- (item) =>
223
- (item.cartItemId ||
224
- `item-${
225
- this.options.quoteRequestData.items.indexOf(item) +
226
- 1
227
- }`) === q.cartItemId
228
- );
229
- return {
230
- quoteId: q.id,
231
- productPrice: originalItem?.productPrice || q.spotPrice,
232
- cartItemId: q.cartItemId,
233
- };
234
- }
235
- );
236
- }
237
-
238
- this.options.callbacks.onOptIn(optInData);
239
- }
240
- }
241
- if (val === "no" && this.options.callbacks?.onOptOut) {
242
- const optOutData = {
243
- status: "QUOTE_DECLINED",
244
- quoteId: this.quote.id,
245
- };
246
-
247
- // For batch quotes, include detailed quote information
248
- if (
249
- this.quote.originalQuotes &&
250
- this.quote.originalQuotes.length > 0
251
- ) {
252
- optOutData.batchQuoteDetails = this.quote.originalQuotes.map(
253
- (q) => {
254
- const originalItem = this.options.quoteRequestData.items?.find(
255
- (item) =>
256
- (item.cartItemId ||
257
- `item-${
258
- this.options.quoteRequestData.items.indexOf(item) + 1
259
- }`) === q.cartItemId
260
- );
261
- return {
262
- quoteId: q.id,
263
- productPrice: originalItem?.productPrice || q.spotPrice,
264
- cartItemId: q.cartItemId,
265
- };
266
- }
267
- );
268
- }
269
-
270
- this.options.callbacks.onOptOut(optOutData);
271
- }
272
- });
273
- });
274
- }
275
-
276
- showSelectionError() {
277
- if (!this.errorEl) {
278
- this.errorEl = document.createElement("div");
279
- this.errorEl.className = "spot-selection__error";
280
- this.errorEl.textContent = "Please make a selection";
281
- const optionsContainer = this.container?.querySelector(
282
- ".spot-selection__options"
283
- );
284
- if (optionsContainer) {
285
- optionsContainer.insertAdjacentElement("afterend", this.errorEl);
286
- }
287
- }
288
-
289
- this.errorEl.style.display = "block";
290
- }
291
-
292
- hideSelectionError() {
293
- if (this.errorEl) {
294
- this.errorEl.style.display = "none";
295
- }
296
- }
297
-
298
- validateSelection() {
299
- if (!this.container) return false;
300
-
301
- const isSelected = !!this.container.querySelector(
302
- 'input[name="selection"]:checked'
303
- );
304
-
305
- if (!isSelected) {
306
- this.showSelectionError();
307
- } else {
308
- this.hideSelectionError();
309
- }
310
-
311
- return isSelected;
312
- }
313
-
314
- async updateQuote(newQuoteRequestData) {
315
- try {
316
- const updatedOptions = {
317
- ...this.options,
318
- quoteRequestData: newQuoteRequestData,
319
- };
320
-
321
- validateOptions(updatedOptions);
322
-
323
- const {
324
- environment,
325
- partnerId,
326
- endpoint: customEndpoint,
327
- } = this.options.apiConfig;
328
-
329
- const endpoint = customEndpoint || apiEndpoint[environment];
330
-
331
- const isBatchQuote =
332
- updatedOptions.quoteRequestData.cartInfo &&
333
- updatedOptions.quoteRequestData.items;
334
- const response = isBatchQuote
335
- ? await fetchMultipleQuotes(
336
- endpoint,
337
- partnerId,
338
- updatedOptions.quoteRequestData
339
- )
340
- : await fetchQuote(
341
- endpoint,
342
- partnerId,
343
- updatedOptions.quoteRequestData
344
- );
345
-
346
- if (response.status !== "QUOTE_AVAILABLE") {
347
- if (
348
- response.status === "NO_MATCHING_QUOTE" &&
349
- this.options.callbacks?.noMatchingQuote
350
- ) {
351
- this.options.callbacks.noMatchingQuote({
352
- status: "NO_MATCHING_QUOTE",
353
- data: updatedOptions.quoteRequestData,
354
- });
355
- }
356
- return false;
357
- }
358
-
359
- this.options.quoteRequestData = updatedOptions.quoteRequestData;
360
- this.quote = response.data;
361
- this.currentSelection = null;
362
-
363
- this.destroy();
364
-
365
- this._renderWidget();
366
-
367
- if (this.options.callbacks?.onQuoteRetrieved) {
368
- this.options.callbacks.onQuoteRetrieved(this.quote);
369
- }
370
-
371
- return true;
372
- } catch (err) {
373
- this.options.callbacks?.onError({
374
- message: err.message,
375
- status: err.status,
376
- responseBody: err.responseBody,
377
- });
378
- return false;
379
- }
380
- }
381
-
382
- getSelection() {
383
- if (this.currentSelection == null) return null;
384
-
385
- const selectionData = {
386
- selection: this.currentSelection,
387
- quoteId: this.quote?.id,
388
- spotPrice: this.quote?.spotPrice,
389
- status:
390
- this.currentSelection === "yes" ? "QUOTE_ACCEPTED" : "QUOTE_DECLINED",
391
- };
392
-
393
- // For batch quotes, include detailed quote information
394
- if (this.quote?.originalQuotes && this.quote.originalQuotes.length > 0) {
395
- selectionData.batchQuoteDetails = this.quote.originalQuotes.map((q) => {
396
- const originalItem = this.options.quoteRequestData.items?.find(
397
- (item) =>
398
- (item.cartItemId ||
399
- `item-${
400
- this.options.quoteRequestData.items.indexOf(item) + 1
401
- }`) === q.cartItemId
402
- );
403
- return {
404
- quoteId: q.id,
405
- productPrice: originalItem?.productPrice || q.spotPrice,
406
- cartItemId: q.cartItemId,
407
- };
408
- });
409
- }
410
-
411
- return selectionData;
412
- }
413
-
414
- destroy() {
415
- window.removeEventListener("resize", this._onResize);
416
- if (this.container && this.container.parentNode) {
417
- this.container.parentNode.removeChild(this.container);
418
- }
419
- }
420
- }
421
-
422
- export default SpotWidget;