| 1 |
/** |
| 2 |
* Shared plan-group + selling-plan (term) form logic. |
| 3 |
* |
| 4 |
* Owns the Create-Plan-Group modal and the Add / Edit Duration modal: |
| 5 |
* plan-type selection, field collection, the /groups and /terms REST writes, |
| 6 |
* and prefilling. Used by BOTH the Plans admin screen |
| 7 |
* (assets/js/admin/plans.js) and the product-editor Subscription tab |
| 8 |
* (assets/js/admin/product-plans.js), so the term payload contract lives in a |
| 9 |
* single place. |
| 10 |
* |
| 11 |
* On success it does NOT navigate. It closes the modal and dispatches a DOM |
| 12 |
* event so each host page decides what happens next: |
| 13 |
* - "subscrpt:group-created" detail: { group } |
| 14 |
* - "subscrpt:term-saved" detail: { groupId, termId, editing } |
| 15 |
* |
| 16 |
* Config is read from window.subscrptPlanForms (restUrl, nonce, i18n), |
| 17 |
* localized on every page that renders these modals. |
| 18 |
*/ |
| 19 |
(function () { |
| 20 |
"use strict"; |
| 21 |
|
| 22 |
var cfg = window.subscrptPlanForms || {}; |
| 23 |
var i18n = cfg.i18n || {}; |
| 24 |
|
| 25 |
// day/week/month/year ⇄ billing_interval integer. |
| 26 |
var INTERVAL_TO_INT = { day: 1, week: 2, month: 3, year: 4 }; |
| 27 |
var INT_TO_INTERVAL = { 1: "day", 2: "week", 3: "month", 4: "year" }; |
| 28 |
|
| 29 |
// REST calls, the button-busy lock and every message here come from the |
| 30 |
// shared component (admin-components/save.js). |
| 31 |
var save = window.WPSubsSave.bind({ restUrl: cfg.restUrl, nonce: cfg.nonce, i18n: i18n }); |
| 32 |
var api = save.api; |
| 33 |
var setLoading = save.busy; |
| 34 |
|
| 35 |
/* ------------------------------------------------------------------ * |
| 36 |
* One-time purchase: the prices follow the toggle. |
| 37 |
* ------------------------------------------------------------------ */ |
| 38 |
|
| 39 |
/** |
| 40 |
* The block one of these controls belongs to. Three shapes carry it — a |
| 41 |
* section below the table in the product editor, and on the Plans screen a |
| 42 |
* table row (variable) or a card (simple) — so they all carry one hook. |
| 43 |
* |
| 44 |
* @param {HTMLElement} el A control inside the block. |
| 45 |
* @return {HTMLElement|null} |
| 46 |
*/ |
| 47 |
function oneTimeScope(el) { |
| 48 |
return el.closest("[data-subscrpt-onetime]"); |
| 49 |
} |
| 50 |
|
| 51 |
/** |
| 52 |
* Fill a one-time block's empty price fields from the product's current |
| 53 |
* WooCommerce price. |
| 54 |
* |
| 55 |
* The one-time price IS the product's native price, so an empty field is not |
| 56 |
* a choice — it is the merchant being asked to retype what the product |
| 57 |
* already costs. The General tab's inputs are the source because they carry |
| 58 |
* the price typed in this session, which no server render can know about. |
| 59 |
* Only empty fields are filled, so a saved or hand-entered price stands. |
| 60 |
* |
| 61 |
* On the Plans screen those inputs do not exist and this does nothing; that |
| 62 |
* screen seeds its fields server-side instead. |
| 63 |
* |
| 64 |
* @param {HTMLElement} scope A one-time row or card. |
| 65 |
*/ |
| 66 |
function seedOneTimePrices(scope) { |
| 67 |
[ |
| 68 |
["price", "_regular_price"], |
| 69 |
["offer", "_sale_price"], |
| 70 |
].forEach(function (pair) { |
| 71 |
var input = scope.querySelector('[data-ot-field="' + pair[0] + '"]'); |
| 72 |
var native = document.getElementById(pair[1]); |
| 73 |
if (!input || !native || "" !== String(input.value).trim()) { |
| 74 |
return; |
| 75 |
} |
| 76 |
var value = String(native.value).trim(); |
| 77 |
if ("" !== value) { |
| 78 |
input.value = value; |
| 79 |
} |
| 80 |
}); |
| 81 |
} |
| 82 |
|
| 83 |
/** |
| 84 |
* Show the prices only while one-time purchase is switched on. |
| 85 |
* |
| 86 |
* Off, the price fields read as a second regular price sitting under the |
| 87 |
* plan's own — the merchant has no way to tell they do not apply. Revealing |
| 88 |
* them on the toggle says which is which. |
| 89 |
* |
| 90 |
* @param {HTMLElement} scope A one-time row or card. |
| 91 |
*/ |
| 92 |
function syncOneTime(scope) { |
| 93 |
var toggle = scope.querySelector("[data-subscrpt-onetime-enable]"); |
| 94 |
if (!toggle) { |
| 95 |
return; |
| 96 |
} |
| 97 |
var on = toggle.checked; |
| 98 |
|
| 99 |
if (on) { |
| 100 |
seedOneTimePrices(scope); |
| 101 |
} |
| 102 |
|
| 103 |
scope.querySelectorAll("[data-subscrpt-onetime-price]").forEach(function (el) { |
| 104 |
el.style.display = on ? "" : "none"; |
| 105 |
}); |
| 106 |
|
| 107 |
// The card keeps its whole body behind the toggle, prices and all. |
| 108 |
var body = scope.querySelector("[data-subscrpt-onetime-body]"); |
| 109 |
if (body) { |
| 110 |
body.style.display = on ? "" : "none"; |
| 111 |
} |
| 112 |
} |
| 113 |
|
| 114 |
// Lives here because both the Plans screen and the product editor load this |
| 115 |
// module, and both render the block. |
| 116 |
document.addEventListener("change", function (e) { |
| 117 |
var toggle = e.target.closest("[data-subscrpt-onetime-enable]"); |
| 118 |
var scope = toggle && oneTimeScope(toggle); |
| 119 |
if (scope) { |
| 120 |
syncOneTime(scope); |
| 121 |
} |
| 122 |
}); |
| 123 |
|
| 124 |
/** |
| 125 |
* Open a modal by id via the shared helper. |
| 126 |
* |
| 127 |
* @param {string} id Modal id. |
| 128 |
*/ |
| 129 |
function openModal(id) { |
| 130 |
if (window.WPSubsModal && typeof window.WPSubsModal.open === "function") { |
| 131 |
window.WPSubsModal.open(id); |
| 132 |
} |
| 133 |
} |
| 134 |
|
| 135 |
/** |
| 136 |
* Close a modal by id via the shared helper. |
| 137 |
* |
| 138 |
* @param {string} id Modal id. |
| 139 |
*/ |
| 140 |
function closeModal(id) { |
| 141 |
if (window.WPSubsModal && typeof window.WPSubsModal.close === "function") { |
| 142 |
window.WPSubsModal.close(id); |
| 143 |
} |
| 144 |
} |
| 145 |
|
| 146 |
/** |
| 147 |
* Read the current value of an advanced-select (its hidden input). |
| 148 |
* |
| 149 |
* @param {HTMLElement} root .wpsubs-adv-select root. |
| 150 |
* @return {string} |
| 151 |
*/ |
| 152 |
function advValue(root) { |
| 153 |
var hidden = root.querySelector('input[type="hidden"]'); |
| 154 |
return hidden ? hidden.value : ""; |
| 155 |
} |
| 156 |
|
| 157 |
/** |
| 158 |
* Programmatically set an advanced-select's value + visible label. |
| 159 |
* |
| 160 |
* @param {HTMLElement} root .wpsubs-adv-select root. |
| 161 |
* @param {string} value Option value to select. |
| 162 |
*/ |
| 163 |
function setAdvSelect(root, value) { |
| 164 |
value = value == null ? "" : String(value); |
| 165 |
var hidden = root.querySelector('input[type="hidden"]'); |
| 166 |
if (hidden) { |
| 167 |
hidden.value = value; |
| 168 |
} |
| 169 |
var label = root.querySelector(".wpsubs-adv-select__label"); |
| 170 |
var item = root.querySelector('.wpsubs-adv-select__item[data-value="' + value + '"]'); |
| 171 |
if (label) { |
| 172 |
label.textContent = item ? item.textContent.trim() : root.getAttribute("data-placeholder") || ""; |
| 173 |
} |
| 174 |
} |
| 175 |
|
| 176 |
/** |
| 177 |
* Collect all [data-subscrpt-field] values within a scope into a flat map. |
| 178 |
* |
| 179 |
* @param {HTMLElement} scope Container (the term modal). |
| 180 |
* @return {Object} |
| 181 |
*/ |
| 182 |
function collectFields(scope) { |
| 183 |
var out = {}; |
| 184 |
scope.querySelectorAll("[data-subscrpt-field]").forEach(function (el) { |
| 185 |
var key = el.getAttribute("data-subscrpt-field"); |
| 186 |
if (el.classList.contains("wpsubs-adv-select")) { |
| 187 |
out[key] = advValue(el); |
| 188 |
} else if (el.type === "checkbox") { |
| 189 |
out[key] = el.checked; |
| 190 |
} else if (el.type === "radio") { |
| 191 |
if (el.checked) { |
| 192 |
out[key] = el.value; |
| 193 |
} |
| 194 |
} else { |
| 195 |
out[key] = el.value; |
| 196 |
} |
| 197 |
}); |
| 198 |
return out; |
| 199 |
} |
| 200 |
|
| 201 |
/** |
| 202 |
* Map the term modal's flat fields to a /terms payload. |
| 203 |
* |
| 204 |
* @param {Object} f Flat field map. |
| 205 |
* @param {string} groupId Plan group id. |
| 206 |
* @param {string} groupType Plan group type key. |
| 207 |
* @return {Object} REST payload. |
| 208 |
*/ |
| 209 |
function termPayload(f, groupId, groupType) { |
| 210 |
var data = { |
| 211 |
free_trial_interval: f.free_trial_interval || "day", |
| 212 |
}; |
| 213 |
|
| 214 |
// Recurring Delivery: separate delivery schedule + sync toggle. |
| 215 |
if (typeof f.delivery_sync !== "undefined") { |
| 216 |
data.delivery_sync = !!f.delivery_sync; |
| 217 |
if (data.delivery_sync && typeof f.delivery_day !== "undefined") { |
| 218 |
data.delivery_day = f.delivery_day; |
| 219 |
} |
| 220 |
} |
| 221 |
if (typeof f.delivery_frequency !== "undefined") { |
| 222 |
var deliveryFreq = parseInt(f.delivery_frequency, 10); |
| 223 |
if (!String(f.delivery_frequency).trim() || isNaN(deliveryFreq) || deliveryFreq < 1) { |
| 224 |
// Empty delivery schedule -> mirror the billing schedule. |
| 225 |
data.delivery_frequency = parseInt(f.billing_frequency, 10) || 1; |
| 226 |
data.delivery_interval = f.billing_interval || "month"; |
| 227 |
} else { |
| 228 |
data.delivery_frequency = deliveryFreq; |
| 229 |
data.delivery_interval = f.delivery_interval || "month"; |
| 230 |
} |
| 231 |
} |
| 232 |
|
| 233 |
// Split Payment: number of payments + access-ends timing. |
| 234 |
if (typeof f.installment_count !== "undefined") { |
| 235 |
data.installment_count = Math.max(2, parseInt(f.installment_count, 10) || 2); |
| 236 |
} |
| 237 |
if (typeof f.access_ends !== "undefined") { |
| 238 |
data.access_ends = f.access_ends || "lifetime"; |
| 239 |
if ("custom" === data.access_ends) { |
| 240 |
data.access_custom_value = parseInt(f.access_custom_value, 10) || 1; |
| 241 |
data.access_custom_interval = f.access_custom_interval || "month"; |
| 242 |
} |
| 243 |
} |
| 244 |
|
| 245 |
return { |
| 246 |
plan_group_id: parseInt(groupId, 10), |
| 247 |
type: groupType || "recurring", |
| 248 |
title: f.title || "", |
| 249 |
billing_frequency: parseInt(f.billing_frequency, 10) || 1, |
| 250 |
billing_interval: INTERVAL_TO_INT[f.billing_interval] || 3, |
| 251 |
billing_length: parseInt(f.billing_length, 10) || 0, |
| 252 |
free_trial: f.free_trial || "", |
| 253 |
signup_fee: { amount: f.signup_fee_amount || "" }, |
| 254 |
status: "active", |
| 255 |
data: data, |
| 256 |
}; |
| 257 |
} |
| 258 |
|
| 259 |
/** |
| 260 |
* Prefill the term modal from a fetched term, or clear it for "add". |
| 261 |
* |
| 262 |
* @param {HTMLElement} modal Term modal. |
| 263 |
* @param {Object|null} term Term row, or null to reset. |
| 264 |
*/ |
| 265 |
function fillTermModal(modal, term) { |
| 266 |
modal.querySelectorAll("[data-subscrpt-field]").forEach(function (el) { |
| 267 |
var key = el.getAttribute("data-subscrpt-field"); |
| 268 |
var adv = el.classList.contains("wpsubs-adv-select"); |
| 269 |
|
| 270 |
// Reset to defaults for "add". |
| 271 |
if (!term) { |
| 272 |
if (adv) { |
| 273 |
setAdvSelect(el, el.getAttribute("data-default-value") || ""); |
| 274 |
} else if (el.type === "checkbox") { |
| 275 |
el.checked = false; |
| 276 |
} else if (el.type !== "radio") { |
| 277 |
el.value = key === "billing_frequency" || key === "billing_length" ? el.defaultValue : ""; |
| 278 |
} |
| 279 |
return; |
| 280 |
} |
| 281 |
|
| 282 |
var data = term.data || {}; |
| 283 |
switch (key) { |
| 284 |
case "title": |
| 285 |
el.value = term.title || ""; |
| 286 |
break; |
| 287 |
case "billing_frequency": |
| 288 |
el.value = term.billing_frequency || 1; |
| 289 |
break; |
| 290 |
case "billing_interval": |
| 291 |
setAdvSelect(el, INT_TO_INTERVAL[term.billing_interval] || "month"); |
| 292 |
break; |
| 293 |
case "billing_length": |
| 294 |
el.value = term.billing_length || 0; |
| 295 |
break; |
| 296 |
case "free_trial": |
| 297 |
el.value = term.free_trial || ""; |
| 298 |
break; |
| 299 |
case "free_trial_interval": |
| 300 |
setAdvSelect(el, data.free_trial_interval || "day"); |
| 301 |
break; |
| 302 |
case "signup_fee_amount": |
| 303 |
el.value = term.signup_fee && term.signup_fee.amount ? term.signup_fee.amount : ""; |
| 304 |
break; |
| 305 |
case "delivery_sync": |
| 306 |
el.checked = !!data.delivery_sync; |
| 307 |
break; |
| 308 |
case "delivery_day": |
| 309 |
setAdvSelect(el, typeof data.delivery_day !== "undefined" ? data.delivery_day : "1"); |
| 310 |
break; |
| 311 |
case "delivery_frequency": |
| 312 |
el.value = data.delivery_frequency || 1; |
| 313 |
break; |
| 314 |
case "delivery_interval": |
| 315 |
setAdvSelect(el, data.delivery_interval || "month"); |
| 316 |
break; |
| 317 |
case "installment_count": |
| 318 |
el.value = data.installment_count || 2; |
| 319 |
break; |
| 320 |
case "access_ends": |
| 321 |
setAdvSelect(el, data.access_ends || "lifetime"); |
| 322 |
break; |
| 323 |
case "access_custom_value": |
| 324 |
el.value = data.access_custom_value || 1; |
| 325 |
break; |
| 326 |
case "access_custom_interval": |
| 327 |
setAdvSelect(el, data.access_custom_interval || "month"); |
| 328 |
break; |
| 329 |
default: |
| 330 |
break; |
| 331 |
} |
| 332 |
}); |
| 333 |
modal.setAttribute("data-editing", term ? term.id : ""); |
| 334 |
toggleAccessCustom(modal); |
| 335 |
toggleDeliveryDay(modal); |
| 336 |
var title = modal.querySelector("[data-subscrpt-term-title]"); |
| 337 |
if (title) { |
| 338 |
title.textContent = term ? i18n.editTerm || "Edit Duration" : i18n.addTerm || "Add Duration"; |
| 339 |
} |
| 340 |
} |
| 341 |
|
| 342 |
/** |
| 343 |
* Show the custom access-length inputs only when "Custom" is selected. |
| 344 |
* |
| 345 |
* @param {HTMLElement} modal Term modal (or any ancestor of the controls). |
| 346 |
*/ |
| 347 |
function toggleAccessCustom(modal) { |
| 348 |
if (!modal) { |
| 349 |
return; |
| 350 |
} |
| 351 |
var sel = modal.querySelector('[data-subscrpt-field="access_ends"]'); |
| 352 |
var custom = modal.querySelector("[data-subscrpt-access-custom]"); |
| 353 |
if (!sel || !custom) { |
| 354 |
return; |
| 355 |
} |
| 356 |
var isCustom = "custom" === advValue(sel); |
| 357 |
custom.style.display = isCustom ? "" : "none"; |
| 358 |
|
| 359 |
// Access ends spans full width unless the custom-duration column is shown. |
| 360 |
var grid = modal.querySelector("[data-subscrpt-access-grid]"); |
| 361 |
if (grid) { |
| 362 |
grid.style.gridTemplateColumns = isCustom ? "1fr 2fr" : "1fr"; |
| 363 |
} |
| 364 |
} |
| 365 |
|
| 366 |
/** |
| 367 |
* Enable the delivery-day picker only when Synchronize schedule is on |
| 368 |
* (it stays visible but disabled otherwise). |
| 369 |
* |
| 370 |
* @param {HTMLElement} modal Term modal. |
| 371 |
*/ |
| 372 |
function toggleDeliveryDay(modal) { |
| 373 |
if (!modal) { |
| 374 |
return; |
| 375 |
} |
| 376 |
var box = modal.querySelector('[data-subscrpt-field="delivery_sync"]'); |
| 377 |
var day = modal.querySelector("[data-subscrpt-delivery-day]"); |
| 378 |
if (!box || !day) { |
| 379 |
return; |
| 380 |
} |
| 381 |
var on = box.checked; |
| 382 |
day.style.opacity = on ? "" : "0.55"; |
| 383 |
day.style.pointerEvents = on ? "" : "none"; |
| 384 |
} |
| 385 |
|
| 386 |
/** |
| 387 |
* Reset the term modal for a specific group and open it (used by callers |
| 388 |
* that chain "create group → add first plan", e.g. the product editor). |
| 389 |
* |
| 390 |
* @param {number|string} groupId Plan group id. |
| 391 |
* @param {string} groupType Plan group type key. |
| 392 |
*/ |
| 393 |
function openTermModalForGroup(groupId, groupType) { |
| 394 |
var modal = document.querySelector("[data-subscrpt-term-modal]"); |
| 395 |
if (!modal) { |
| 396 |
return; |
| 397 |
} |
| 398 |
modal.setAttribute("data-group-id", groupId); |
| 399 |
modal.setAttribute("data-group-type", groupType || "recurring"); |
| 400 |
fillTermModal(modal, null); |
| 401 |
openModal("subscrpt-term-modal"); |
| 402 |
} |
| 403 |
|
| 404 |
/* ------------------------------------------------------------------ * |
| 405 |
* Handlers (delegated on document; each host page renders the modals). |
| 406 |
* ------------------------------------------------------------------ */ |
| 407 |
|
| 408 |
// Plan-type cards: click to select (skips locked / Pro cards). Selection is a |
| 409 |
// border + soft brand background - no radio input. |
| 410 |
document.addEventListener("click", function (e) { |
| 411 |
var card = e.target.closest(".subscrpt-type-card"); |
| 412 |
if (!card || card.hasAttribute("data-locked")) { |
| 413 |
return; |
| 414 |
} |
| 415 |
var list = card.closest("[data-subscrpt-type-list]"); |
| 416 |
if (!list) { |
| 417 |
return; |
| 418 |
} |
| 419 |
var typeLabels = []; |
| 420 |
list.querySelectorAll(".subscrpt-type-card").forEach(function (c) { |
| 421 |
var on = c === card; |
| 422 |
c.classList.toggle("is-selected", on); |
| 423 |
c.style.borderColor = on ? "var(--wpsubs-brand)" : "var(--wpsubs-border)"; |
| 424 |
c.style.background = on ? "var(--wpsubs-brand-light)" : ""; |
| 425 |
var icon = c.querySelector(".dashicons"); |
| 426 |
if (icon) { |
| 427 |
icon.style.color = on ? "var(--wpsubs-brand)" : "var(--wpsubs-text-subtle)"; |
| 428 |
} |
| 429 |
var lbl = c.getAttribute("data-subscrpt-type-label"); |
| 430 |
if (lbl) { |
| 431 |
typeLabels.push(lbl); |
| 432 |
} |
| 433 |
}); |
| 434 |
|
| 435 |
// Auto-fill the name from the selected type — but only while it is empty or |
| 436 |
// still holds an auto-generated type label, so a name the user typed (or a |
| 437 |
// restored one) is never overwritten. |
| 438 |
var modal = card.closest(".wpsubs-modal"); |
| 439 |
var nameInput = modal ? modal.querySelector("#subscrpt-create-name") : null; |
| 440 |
var selectedLabel = card.getAttribute("data-subscrpt-type-label"); |
| 441 |
if (nameInput && selectedLabel) { |
| 442 |
var current = nameInput.value.trim(); |
| 443 |
if (current === "" || typeLabels.indexOf(current) !== -1) { |
| 444 |
nameInput.value = selectedLabel; |
| 445 |
} |
| 446 |
} |
| 447 |
}); |
| 448 |
|
| 449 |
// Create a plan group. On success, close the modal and let the host decide |
| 450 |
// where to go next (Plans page redirects to the detail; the product editor |
| 451 |
// chains into the Add-Selling-Plan modal). |
| 452 |
document.addEventListener("click", function (e) { |
| 453 |
var btn = e.target.closest("[data-subscrpt-create-plan]"); |
| 454 |
if (!btn) { |
| 455 |
return; |
| 456 |
} |
| 457 |
e.preventDefault(); |
| 458 |
|
| 459 |
var modal = btn.closest(".wpsubs-modal"); |
| 460 |
var nameInput = modal.querySelector("#subscrpt-create-name"); |
| 461 |
var name = nameInput ? nameInput.value.trim() : ""; |
| 462 |
|
| 463 |
if (!name) { |
| 464 |
save.notify(i18n.nameRequired, "error"); |
| 465 |
return; |
| 466 |
} |
| 467 |
|
| 468 |
// Selected type card (defaults to recurring). The REST controller rejects |
| 469 |
// non-recurring types unless Pro is active. |
| 470 |
var selectedCard = modal.querySelector(".subscrpt-type-card.is-selected"); |
| 471 |
var planType = selectedCard ? selectedCard.getAttribute("data-subscrpt-type") : "recurring"; |
| 472 |
|
| 473 |
// Deferred mode (product-editor wizard): don't create yet. Hand the group |
| 474 |
// details to step 2, which creates the group + first plan atomically on |
| 475 |
// submit, so nothing is created until both steps are filled. |
| 476 |
if (modal.hasAttribute("data-subscrpt-defer")) { |
| 477 |
closeModal("subscrpt-create-plan"); |
| 478 |
document.dispatchEvent( |
| 479 |
new CustomEvent("subscrpt:group-step", { |
| 480 |
detail: { title: name, type: planType || "recurring" }, |
| 481 |
}), |
| 482 |
); |
| 483 |
return; |
| 484 |
} |
| 485 |
|
| 486 |
setLoading(btn, true); |
| 487 |
api("POST", "/groups", { |
| 488 |
title: name, |
| 489 |
type: planType || "recurring", |
| 490 |
product_type: 1, |
| 491 |
status: "active", |
| 492 |
}) |
| 493 |
.then(function (group) { |
| 494 |
setLoading(btn, false); |
| 495 |
if (nameInput) { |
| 496 |
nameInput.value = ""; |
| 497 |
} |
| 498 |
closeModal("subscrpt-create-plan"); |
| 499 |
document.dispatchEvent(new CustomEvent("subscrpt:group-created", { detail: { group: group } })); |
| 500 |
}) |
| 501 |
.catch(function (err) { |
| 502 |
setLoading(btn, false); |
| 503 |
save.notify(err.message || i18n.genericError, "error"); |
| 504 |
}); |
| 505 |
}); |
| 506 |
|
| 507 |
// Re-evaluate the custom access row when the selection changes. |
| 508 |
document.addEventListener("wpsubs:select", function (e) { |
| 509 |
var root = e.target.closest('[data-subscrpt-field="access_ends"]'); |
| 510 |
if (root) { |
| 511 |
toggleAccessCustom(root.closest("[data-subscrpt-term-modal]")); |
| 512 |
} |
| 513 |
}); |
| 514 |
|
| 515 |
// Toggle the delivery-day picker as the sync checkbox changes. |
| 516 |
document.addEventListener("change", function (e) { |
| 517 |
var box = e.target.closest('[data-subscrpt-field="delivery_sync"]'); |
| 518 |
if (box) { |
| 519 |
toggleDeliveryDay(box.closest("[data-subscrpt-term-modal]")); |
| 520 |
} |
| 521 |
}); |
| 522 |
|
| 523 |
// Open the term modal in add or edit mode. |
| 524 |
document.addEventListener("click", function (e) { |
| 525 |
var add = e.target.closest("[data-subscrpt-add-term]"); |
| 526 |
var edit = e.target.closest("[data-subscrpt-edit-term]"); |
| 527 |
if (!add && !edit) { |
| 528 |
return; |
| 529 |
} |
| 530 |
var modal = document.querySelector("[data-subscrpt-term-modal]"); |
| 531 |
if (!modal) { |
| 532 |
return; |
| 533 |
} |
| 534 |
if (add) { |
| 535 |
fillTermModal(modal, null); |
| 536 |
// The trigger already carries data-wpsubs-modal-open, so the shared |
| 537 |
// WPSubsModal opens it. |
| 538 |
return; |
| 539 |
} |
| 540 |
e.preventDefault(); |
| 541 |
var termId = edit.getAttribute("data-subscrpt-edit-term"); |
| 542 |
api("GET", "/terms/" + termId) |
| 543 |
.then(function (term) { |
| 544 |
fillTermModal(modal, term); |
| 545 |
openModal("subscrpt-term-modal"); |
| 546 |
}) |
| 547 |
.catch(function (err) { |
| 548 |
save.notify(err.message || i18n.genericError, "error"); |
| 549 |
}); |
| 550 |
}); |
| 551 |
|
| 552 |
// Save the term (create or update). On success, close the modal and let the |
| 553 |
// host decide (Plans page reloads; the product editor refreshes in place). |
| 554 |
document.addEventListener("click", function (e) { |
| 555 |
var btn = e.target.closest("[data-subscrpt-term-submit]"); |
| 556 |
if (!btn) { |
| 557 |
return; |
| 558 |
} |
| 559 |
var modal = btn.closest("[data-subscrpt-term-modal]"); |
| 560 |
var groupId = modal.getAttribute("data-group-id"); |
| 561 |
var groupType = modal.getAttribute("data-group-type"); |
| 562 |
var editing = modal.getAttribute("data-editing"); |
| 563 |
var payload = termPayload(collectFields(modal), groupId, groupType); |
| 564 |
|
| 565 |
if (!payload.title) { |
| 566 |
save.notify(i18n.nameRequired, "error"); |
| 567 |
return; |
| 568 |
} |
| 569 |
|
| 570 |
// Deferred mode (product-editor wizard): hand the plan payload to the host, |
| 571 |
// which creates the group + this plan atomically. Nothing is written here. |
| 572 |
if (modal.hasAttribute("data-subscrpt-defer")) { |
| 573 |
closeModal("subscrpt-term-modal"); |
| 574 |
document.dispatchEvent(new CustomEvent("subscrpt:term-step", { detail: { payload: payload } })); |
| 575 |
return; |
| 576 |
} |
| 577 |
|
| 578 |
setLoading(btn, true); |
| 579 |
var req = editing ? api("PUT", "/terms/" + editing, payload) : api("POST", "/terms", payload); |
| 580 |
req |
| 581 |
.then(function (term) { |
| 582 |
setLoading(btn, false); |
| 583 |
closeModal("subscrpt-term-modal"); |
| 584 |
document.dispatchEvent( |
| 585 |
new CustomEvent("subscrpt:term-saved", { |
| 586 |
detail: { groupId: groupId, termId: term && term.id, editing: editing }, |
| 587 |
}), |
| 588 |
); |
| 589 |
}) |
| 590 |
.catch(function (err) { |
| 591 |
setLoading(btn, false); |
| 592 |
save.notify(err.message || i18n.genericError, "error"); |
| 593 |
}); |
| 594 |
}); |
| 595 |
|
| 596 |
window.WPSubsPlanForms = { |
| 597 |
api: api, |
| 598 |
seedOneTimePrices: seedOneTimePrices, |
| 599 |
fillTermModal: fillTermModal, |
| 600 |
openModal: openModal, |
| 601 |
closeModal: closeModal, |
| 602 |
openTermModalForGroup: openTermModalForGroup, |
| 603 |
}; |
| 604 |
})(); |
| 605 |
|