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