PageSourceSearch

https://lineageprovisions.com/cdn/shop/t/134/assets/skio-add-to-su…?v=149746686293585459321783538328

js lineageprovisions.com collected 2026-09-25 14:20:27 UTC 22,629 bytes, 514 lines download raw bytes

1import{LitElement,css,html}from"https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js";class SkioAddToSubscription extends LitElement{static styles=css`
2    :host {
3      --color-primary:#111111; 
4      --color-accent: #fff;
5      --color-bg: #f7f2ee; 
6      --color-card-bg: #f5f5f5; 
7      --color-border: #e0e0e0; 
8      --color-text: #333; 
9      --color-muted: #666; 
10      --color-strikethrough: #999; 
11      position: relative;
12      display: block;
13      font-family: inherit;
14      font-size: 16px;
15    }
16    .callout {
17      background: var(--color-card-bg);
18      border-radius: 8px;
19      padding: 24px;
20      margin-bottom: 24px;
21      border: 1px solid var(--color-border);
22      position: relative;
23    }
24    .callout h3 {
25      margin: 0;
26      font-size: 1em;
27      font-weight: 700;
28      color: var(--color-text);
29    }
30    .callout small {
31      display: block;
32      margin-bottom: 16px;
33    }
34    .callout p {
35      margin: 0 0 16px 0;
36      color: var(--color-muted);
37      font-size: 0.9em;
38    }
39    .add-to-subscription-button {
40      background: var(--color-primary);
41      color: var(--color-accent);
42      border: none;
43      border-radius: 4px;
44      padding: 12px 0;
45      width: 100%;
46      font-size: 0.9em;
47      font-weight: 700;
48      cursor: pointer;
49      transition: background 0.2s;
50      text-transform: uppercase;
51    }
52    .add-to-subscription-button:hover {
53      background: color-mix(in srgb, var(--color-primary) 80%, black);
54    }
55    dialog {
56      position: fixed;
57      top: 50%;
58      left: 50%;
59      transform: translate(-50%, -50%);
60      margin: 0;
61      z-index: 2147483647;
62      background: var(--color-accent);
63      border-radius: 12px;
64      border: none;
65      width: 100%;
66      max-width: 500px;
67      padding: 0;
68      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
69    }
70    dialog::backdrop {
71      background: rgba(0, 0, 0, 0.5);
72    }
73    .modal-content {
74      position: relative;
75    }
76    .modal-header {
77      display: flex;
78      justify-content: space-between;
79      align-items: center;
80      gap: 16px;
81      padding: 16px 32px 16px 32px;
82      border-bottom: 1px solid var(--color-border);
83    }
84    .modal-title {
85      font-size: 1.1em;
86      font-weight: 700;
87      color: var(--color-text);
88      margin: 0;
89      line-height: 1.3;
90    }
91    .modal-title .date-badge {
92      border: 2px solid var(--color-primary);
93      border-radius: 4px;
94      padding: 2px 8px;
95      font-size: 0.8em;
96      color: var(--color-primary);
97      font-weight: 500;
98      background: var(--color-accent);
99      display: inline-block;
100    }
101    .close-button {
102      background: none;
103      border: none;
104      font-size: 2em;
105      color: var(--color-muted);
106      cursor: pointer;
107      padding: 0;
108      margin: 0;
109      width: 2em;
110      height: 2em;
111      display: flex;
112      align-items: center;
113      justify-content: center;
114      line-height: 1;
115    }
116    .close-button:hover {
117      color: var(--color-text);
118    }
119    form {
120      padding: 24px 32px;
121      display: flex;
122      flex-direction: column;
123      gap: 16px;
124    }
125    .option-label {
126      display: flex;
127      align-items: center;
128      gap: 8px;
129      background: var(--color-card-bg);
130      border: 1px solid var(--color-border);
131      border-radius: 8px;
132      padding: 12px;
133      cursor: pointer;
134      transition: border 0.2s, background 0.2s;
135      position: relative;
136    }
137    .option-label:hover {
138      border-color: var(--color-primary);
139    }
140    .option-label.selected {
141      border: 2px solid var(--color-primary);
142      background: var(--color-accent);
143    }
144    .option-label input[type="radio"] {
145      accent-color: var(--color-primary);
146      width: 1.5em;
147      height: 1.5em;
148      flex-shrink: 0;
149    }
150    .option-content {
151      display: flex;
152      align-items: flex-start;
153      flex-direction: column;
154      gap: 4px;
155    }
156    .option-title {
157      font-size: 1em;
158      font-weight: 700;
159      color: var(--color-text);
160    }
161    .delivery-policy {
162      font-size: 0.8em;
163      color: var(--color-muted);
164      font-weight: 500;
165      text-transform: capitalize;
166    }
167    .discount-badge {
168      background: var(--color-primary);
169      color: var(--color-accent);
170      font-weight: 700;
171      border-radius: 4px;
172      padding: 4px 12px;
173      font-size: 0.8em;
174      text-transform: uppercase;
175      letter-spacing: 0.5px;
176    }
177    .price-container {
178      margin-left: auto;
179      text-align: right;
180      display: flex;
181      flex-direction: column;
182      align-items: flex-end;
183      gap: 2px;
184    }
185    .original-price {
186      text-decoration: line-through;
187      color: var(--color-strikethrough);
188      font-size: 0.9em;
189      font-weight: 500;
190    }
191    .discounted-price {
192      color: var(--color-primary);
193      font-size: 1em;
194      font-weight: 700;
195    }
196    .modal-policy {
197      font-size: 0.8em;
198      color: var(--color-muted);
199      padding: 0px 32px 16px 32px;
200    }
201    .modal-footer {
202      display: flex;
203      gap: 16px;
204      padding: 24px 32px 32px 32px;
205      border-top: 1px solid var(--color-border);
206    }
207    .modal-footer button {
208      flex: 1;
209      padding: 16px 24px;
210      border-radius: 4px;
211      font-size: 0.9em;
212      font-weight: 700;
213      border: none;
214      cursor: pointer;
215      transition: all 0.2s;
216      text-transform: uppercase;
217      letter-spacing: 0.5px;
218    }
219    .primary-button {
220      background: var(--color-primary);
221      color: var(--color-accent);
222    }
223    .primary-button:hover {
224      background: color-mix(in srgb, var(--color-primary) 80%, black);
225    }
226    .secondary-button {
227      background: transparent;
228      color: var(--color-text);
229      border: 2px solid var(--color-border);
230    }
231    .secondary-button:hover {
232      background: var(--color-card-bg);
233      border-color: var(--color-primary);
234    }
235    .success-screen {
236      background: #e8f5e8;
237      border: 2px solid #4caf50;
238      border-radius: 8px;
239      padding: 24px;
240      margin-bottom: 24px;
241      text-align: left;
242    }
243    .success-title {
244      font-size: 1em;
245      font-weight: 700;
246      color: var(--color-text);
247      margin: 0 0 16px 0;
248      text-transform: uppercase;
249      letter-spacing: 0.5px;
250    }
251    .success-message {
252      font-size: 0.9em;
253      color: var(--color-text);
254      margin: 0 0 8px 0;
255      line-height: 1.4;
256    }
257    .success-date {
258      font-weight: 700;
259      color: var(--color-text);
260    }
261    .success-link {
262      color: var(--color-primary);
263      text-decoration: none;
264      font-weight: 700;
265      font-size: 0.9em;
266      margin-top: 16px;
267      display: inline-block;
268      transition: color 0.2s;
269    }
270    .success-link:hover {
271      color: color-mix(in srgb, var(--color-primary) 80%, black);
272      text-decoration: underline;
273    }
274    .loading-spinner {
275      display: inline-block;
276      width: 16px;
277      height: 16px;
278      border: 2px solid var(--color-accent);
279      border-radius: 50%;
280      border-top-color: transparent;
281      animation: spin 1s linear infinite;
282      margin-right: 8px;
283    }
284    @keyframes spin {
285      to { transform: rotate(360deg); }
286    }
287    .primary-button:disabled {
288      opacity: 0.6;
289      cursor: not-allowed;
290    }
291    .error-message {
292      background: #fee;
293      border: 1px solid #fcc;
294      border-radius: 4px;
295      padding: 12px;
296      margin-bottom: 16px;
297      color: #c33;
298      font-size: 0.9em;
299      text-align: center;
300    }
301    @media(max-width: 500px) {
302      .modal-footer {
303        flex-direction: column;
304        gap: 8px;
305        padding: 12px 16px 16px 16px
306      }
307      .modal-header {
308        padding: 12px 16px 12px 16px;
309      }
310      form {
311        padding: 12px 16px;
312      }
313      .modal-policy {
314        padding: 0px 16px 16px 16px;
315      }
316    }
317  `;static properties={isLoading:{type:Boolean},isAuthenticated:{type:Boolean},productId:{type:String},product:{type:Object},selectedVariant:{type:Object},upcomingSubscription:{type:Object},eligibleForSubscription:{type:Boolean},eligibleForOneTimeUpsell:{type:Boolean},oneTimeUpsellPercentageOff:{type:Number},selectedOption:{type:String},selectedSellingPlan:{type:Object},showSuccessScreen:{type:Boolean},previouslyAddedProducts:{type:String},isAddingToSubscription:{type:Boolean},addToSubscriptionError:{type:String}};constructor(){super(),this.initializeDefaultState()}async connectedCallback(){super.connectedCallback(),await this.initialize(),this.isLoading=!1,this.bindFormEvents()}initializeDefaultState(){this.isLoading=!1,this.eligibleForSubscription=!1,this.eligibleForOneTimeUpsell=!1,this.selectedOption="subscription",this.showSuccessScreen=!1,this.isAddingToSubscription=!1,this.addToSubscriptionError=null}async initialize(){if(this.checkAuthentication(),!this.isAuthenticated)return;const subscription=await this.fetchUpcomingSubscription();this.upcomingSubscription=subscription;const product=await this.fetchProduct();this.product=product}checkAuthentication(){this.isAuthenticated=!!window.Skio?.storefrontUser?.idToken}async fetchUpcomingSubscription(){const cachedSubscription=localStorage.getItem("upcoming-subscription");if(cachedSubscription)return JSON.parse(cachedSubscription);const query=`
318      query storefrontGetUpcomingSubscription($storefrontUserId: uuid) {
319        Subscriptions(where: {
320          status: {
321            _eq: "ACTIVE"
322          }
323          storefrontUserId: {
324            _eq: $storefrontUserId
325          }
326        }, order_by: {
327          nextBillingDate: asc
328        }, limit: 1) {
329          id
330          status
331          nextBillingDate
332          SubscriptionLines(
333            where: { removedAt: { _is_null: true } }
334            limit: 1
335            order_by: { createdAt: asc }
336          ) {
337            OriginalSellingPlan {
338              PricingPolicies {
339                percentageOff
340                fixedPrice
341              }
342            }
343          }
344          DeliveryPolicy { 
345            interval 
346            intervalCount 
347          }
348          BillingPolicy {
349            interval
350            intervalCount
351          }
352        }
353      }
354    `,variables={storefrontUserId:window.Skio?.storefrontUser?.id};try{const subscription=(await window.Skio.graphQLRequest({query,variables})).data.Subscriptions[0];return localStorage.setItem("upcoming-subscription",JSON.stringify(subscription)),this.isPrepaidSubscription(subscription)?null:subscription}catch(error){return console.error("Error fetching upcoming subscription:",error),null}}async fetchProduct(){const query=`
355      query Products($where: Product_bool_exp) {
356        Products(where: $where) {
357          title
358          canUpsell
359          oneTimeUpsellPercentageOff
360          ProductVariants {
361            enableOneTimeUpsell
362            platformId
363            price
364            compareAtPrice
365            SellingPlanGroupResources(where: {SellingPlanGroup: { name: { _eq: "Subscription" } }}) {
366              SellingPlanGroup {
367                customName
368                SellingPlans {
369                  id
370                  platformId
371                  PricingPolicies {
372                    percentageOff
373                    fixedPrice
374                  }
375                  DeliveryPolicy {
376                    interval
377                    intervalCount
378                  }
379                }
380              }
381            }
382          }
383        }
384      }
385    `,variables={where:{platformId:{_eq:`gid://shopify/Product/${this.productId}`}}};try{const response=await window.Skio.graphQLRequest({query,variables});return console.log("Product data:",response.data.Products[0]),response.data.Products[0]}catch(error){return console.error("Error fetching product:",error),null}}isPrepaidSubscription(subscription){const{DeliveryPolicy,BillingPolicy}=subscription;return DeliveryPolicy.interval!==BillingPolicy.interval||DeliveryPolicy.intervalCount!==BillingPolicy.intervalCount}formatDate(timestamp){const date=new Date(timestamp);return`${date.toLocaleString("en-US",{month:"short"})} ${date.getDate()}, ${date.getFullYear()}`}formatMoney(amount){return new Intl.NumberFormat("en-US",{style:"currency",currency:"USD"}).format(amount)}bindFormEvents(){const variantInput=document.querySelector("[name=id]");variantInput&&(this.updateSelectedVariant(variantInput.value),this.checkVariantEligibility(this.selectedVariant),this.checkPreviouslyAddedProducts(),variantInput.addEventListener("change",e=>{this.updateSelectedVariant(e.target.value),this.checkVariantEligibility(this.selectedVariant),this.checkPreviouslyAddedProducts()}))}checkPreviouslyAddedProducts(){(window.sessionStorage.getItem("skio_upsell_added_products")||"").includes(this.selectedVariant.platformId)?this.showSuccessScreen=!0:this.showSuccessScreen=!1}updateSelectedVariant(variantId){this.selectedVariant=this.product.ProductVariants.find(variant=>variant.platformId.includes(variantId))}checkVariantEligibility(variant){variant&&(this.eligibleForOneTimeUpsell=variant.enableOneTimeUpsell&&this.product.canUpsell,this.eligibleForSubscription=variant.SellingPlanGroupResources.length>0,this.eligibleForOneTimeUpsell||(this.selectedOption="subscription"))}getSubscriptionPrice(){const sellingPlanGroup=this.findMatchingSellingPlanGroup(),sellingPlan=this.findMatchingSellingPlan(sellingPlanGroup);if(!sellingPlan)return{discount:null,price:this.formatMoney(this.selectedVariant.price)};this.selectedSellingPlan=sellingPlan;const pricingPolicy=sellingPlan.PricingPolicies[0];let discount,price;return pricingPolicy.percentageOff?(discount=`${pricingPolicy.percentageOff}%`,price=this.selectedVariant.price*(1-pricingPolicy.percentageOff/100)):pricingPolicy.fixedPrice&&(discount=this.formatMoney(pricingPolicy.fixedPrice),price=this.selectedVariant.price-pricingPolicy.fixedPrice),{discount,price:this.formatMoney(price)}}getOneTimePrice(){if(!this.product.oneTimeUpsellPercentageOff)return{discount:null,price:this.formatMoney(this.selectedVariant.price)};const discount=`${this.product.oneTimeUpsellPercentageOff*100}%`,price=this.selectedVariant.price*(1-this.product.oneTimeUpsellPercentageOff);return{discount,price:this.formatMoney(price)}}
385findMatchingSellingPlanGroup(){return console.log(this.selectedVariant),this.selectedVariant.SellingPlanGroupResources.find(group=>group.SellingPlanGroup.SellingPlans.some(plan=>this.isMatchingDeliveryPolicy(plan.DeliveryPolicy)))}findMatchingSellingPlan(sellingPlanGroup){return sellingPlanGroup?sellingPlanGroup.SellingPlanGroup.SellingPlans.find(plan=>this.isMatchingDeliveryPolicy(plan.DeliveryPolicy)):null}isMatchingDeliveryPolicy(deliveryPolicy){return deliveryPolicy.interval===this.upcomingSubscription.DeliveryPolicy.interval&&deliveryPolicy.intervalCount===this.upcomingSubscription.DeliveryPolicy.intervalCount}async addToSubscription(){this.addToSubscriptionError=null,this.isAddingToSubscription=!0;const productVariantPlatformId=this.selectedVariant.platformId,sellingPlanPlatformId=this.selectedSellingPlan.platformId,isOneTimeUpsell=this.selectedOption==="one-time",subscriptionId=this.upcomingSubscription.id,mutation=`
386      mutation storefrontEditSubscriptionLines($input: EditSubscriptionLinesInput!) {
387        editSubscriptionLines(input: $input) {
388          subscriptionId
389        }
390      }
391    `,variables={input:{subscriptionId,subscriptionLinesToCreate:[{customAttributes:[{key:"add-to-subscription-component",value:"true"}],productVariantPlatformId,quantity:1,isOneTimeUpsell}]}};console.log("Adding to subscription:",{mutation,variables});try{const response=await window.Skio.graphQLRequest({query:mutation,variables});this.closeModal();let previouslyAddedProducts=window.sessionStorage.getItem("skio_upsell_added_products")||"";return window.sessionStorage.setItem("skio_upsell_added_products",`${previouslyAddedProducts}, ${this.selectedVariant.platformId}`),this.showSuccessScreen=!0,response}catch(error){return console.error("Error adding to subscription:",error),this.addToSubscriptionError="Error adding to subscription. Please try again.",error}finally{this.isAddingToSubscription=!1}}openModal(){const dialog=this.shadowRoot.querySelector("dialog");dialog&&(dialog.showModal(),this.setupModalEventListeners(dialog))}closeModal(){const dialog=this.shadowRoot.querySelector("dialog");dialog&&(dialog.close(),this.removeModalEventListeners(dialog))}setupModalEventListeners(dialog){dialog.addEventListener("click",this.handleBackdropClick.bind(this))}removeModalEventListeners(dialog){dialog.removeEventListener("click",this.handleBackdropClick.bind(this))}handleBackdropClick(e){e.target.tagName==="DIALOG"&&this.closeModal()}renderModal(){const oneTimePrice=this.getOneTimePrice(),subscriptionPrice=this.getSubscriptionPrice();return html`
392      <dialog>
393        <div class="modal-content">
394          <div class="modal-header">
395            <h2 class="modal-title">
396              One Click Add to Upcoming Subscription
397              <span class="date-badge"
398                >${this.formatDate(this.upcomingSubscription.nextBillingDate)}</span
399              >
400            </h2>
401            <button class="close-button" @click=${this.closeModal}>
402              &times;
403            </button>
404          </div>
405          <form>
406            ${this.addToSubscriptionError?html`<div class="error-message">${this.addToSubscriptionError}</div>`:null}
407            ${this.eligibleForOneTimeUpsell?this.renderOneTimeOption(oneTimePrice):null}
408            ${this.eligibleForSubscription?this.renderSubscriptionOption(subscriptionPrice):null}
409          </form>
410            
411          <div class="modal-policy">
412            ${this.selectedOption==="subscription"?html`<small>By clicking Add to Order, you’re adding this item to your subscription. You’ll be charged automatically at the listed price and frequency until canceled. All cancellations are subject to the <a href="/policies/subscription-policy">cancellation policy</a>.</small>`:html`<small>By selecting Add to Order, you agree to a one-time charge for this item, which will be added to your next subscription shipment. This item will not renew or be billed again. All cancellations are subject to the <a href="/policies/subscription-policy">cancellation policy</a>.</small>`}
413          </div>
414          
415          <div class="modal-footer">
416            <button 
417              class="primary-button" 
418              @click=${this.addToSubscription}
419              ?disabled=${this.isAddingToSubscription}
420            >
421              ${this.isAddingToSubscription?html`<span class="loading-spinner"></span>`:"ADD TO ORDER"}
422            </button>
423            <button
424              class="secondary-button"
425              @click=${this.closeModal}
426              type="button"
427              ?disabled=${this.isAddingToSubscription}
428            >
429              Cancel
430            </button>
431          </div>
432        </div>
433      </dialog>
434    `}renderOneTimeOption(oneTimePrice){const isSelected=this.selectedOption==="one-time";return html`
435      <label
436        class="option-label ${isSelected?"selected":""}"
437        @click=${()=>this.selectedOption="one-time"}
438      >
439        <input
440          type="radio"
441          name="addType"
442          value="one-time"
443          .checked=${isSelected}
444          @change=${()=>this.selectedOption="one-time"}
445        />
446        <div class="option-content">
447          <span class="option-title">Get One-Time</span>
448        </div>
449        ${oneTimePrice.discount?html`<span class="discount-badge text-2xl"
450                >Save ${oneTimePrice.discount}</span
451              >`:null}
452        <div class="price-container">
453          ${this.getOriginalPrice()===oneTimePrice.price?null:html`<span class="original-price"> ${this.getOriginalPrice()} </span>`}
454          <span class="discounted-price">${oneTimePrice.price}</span>
455        </div>
456      </label>
457    `}renderSubscriptionOption(subscriptionPrice){const isSelected=this.selectedOption==="subscription";return html`
458      <label
459        class="option-label ${isSelected?"selected":""}"
460        @click=${()=>this.selectedOption="subscription"}
461      >
462        <input
463          type="radio"
464          name="addType"
465          value="subscription"
466          .checked=${isSelected}
467          @change=${()=>this.selectedOption="subscription"}
468        />
469        <div class="option-content">
470          <span class="option-title">Subscribe</span>
471          <div class="delivery-policy">
472            Every ${this.upcomingSubscription.DeliveryPolicy.intervalCount}
473            ${this.upcomingSubscription.DeliveryPolicy.interval.toLowerCase()}
474          </div>
475        </div>
476        ${subscriptionPrice.discount?html`<span class="discount-badge"
477                >Save ${subscriptionPrice.discount}</span
478              >`:null}
479        <div class="price-container">
480          ${this.getOriginalPrice()===subscriptionPrice.price?null:html`<span class="original-price"> ${this.getOriginalPrice()} </span>`}
481          <span class="discounted-price">${subscriptionPrice.price}</span>
482        </div>
483      </label>
484    `}getOriginalPrice(){return this.selectedVariant.compareAtPrice?this.formatMoney(this.selectedVariant.compareAtPrice):this.formatMoney(this.selectedVariant.price)}renderSuccessScreen(){return html`
485      <div class="success-screen">
486        <h2 class="success-title">Added to Your Next Order</h2>
487        <p class="success-message">
488          This item will be included in your next subscription order shipment on
489          <span class="success-date"
490            >${this.formatDate(this.upcomingSubscription.nextBillingDate)}</span
491          >
492        </p>
493        <a href="/a/account/subscriptions" class="success-link">
494          View and manage my subscriptions ->
495        </a>
496      </div>
497    `}render(){return this.shouldRender()?this.showSuccessScreen?this.renderSuccessScreen():html`
498      <div class="callout">
499        <h3>One Click Add to Upcoming Subscription</h3>
500        <small>You can easily add this item to your upcoming subscription order and save 10%</small>
501        <p>
502          Next order on:
503          <b>${this.formatDate(this.upcomingSubscription.nextBillingDate)}</b>
504        </p>
505        <button
506          class="add-to-subscription-button"
507          @click=${()=>this.openModal()}
508        >
509          Add To Next Order and Save
510        </button>
511      </div>
512      ${this.renderModal()}
513    `:null}shouldRender(){return this.isAuthenticated&&this.upcomingSubscription&&this.eligibleForSubscription&&this.selectedVariant}}customElements.define("skio-add-to-subscription",SkioAddToSubscription);
514//# sourceMappingURL=/cdn/shop/t/134/assets/skio-add-to-subscription.js.map?v=149746686293585459321783538328

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.