From 363f48386a8ee56d21cf67b6c3b8e47d1de344a2 Mon Sep 17 00:00:00 2001 From: aushamim Date: Wed, 9 Sep 2026 16:13:06 +0600 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20new=20onboarding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/admin/onboarding-wizard.css | 96 ++- assets/js/admin/onboarding-wizard.js | 780 +++++++++----------- includes/Admin/Menu.php | 21 +- includes/Admin/OnboardingAjax.php | 199 +----- includes/Admin/views/onboarding-wizard.php | 783 ++++++++++----------- 5 files changed, 841 insertions(+), 1038 deletions(-) diff --git a/assets/css/admin/onboarding-wizard.css b/assets/css/admin/onboarding-wizard.css index bb66cd93..1d7719a7 100644 --- a/assets/css/admin/onboarding-wizard.css +++ b/assets/css/admin/onboarding-wizard.css @@ -1014,11 +1014,99 @@ justify-content: flex-end; gap: 4px; } -@media (max-width: 680px) { - #subscrpt-section-1 .wpsubs-wizard-card { +@media (max-width: 782px) { + /* Stack the two-column body (form + preview/summary) on narrow screens. */ + .wpsubs-p2-body { grid-template-columns: 1fr; + gap: 20px; } - .wpsubs-p1-right { - display: none; + .wpsubs-p2-preview-col { + position: static; } } + +/* Page 1 plan summary card (right column). */ +.wpsubs-p1-summary-card { + border: 1px solid var(--wpsubs-border); + border-radius: var(--wpsubs-radius); + background: var(--wpsubs-surface); + padding: 16px; + box-shadow: var(--wpsubs-shadow); +} +.wpsubs-p1-summary-type { + display: inline-block; + padding: 3px 9px; + border-radius: 999px; + background: var(--wpsubs-brand-light); + color: var(--wpsubs-brand); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.02em; +} +.wpsubs-p1-summary-name { + font-size: 15px; + font-weight: 700; + color: var(--wpsubs-text); + margin: 10px 0 12px; + line-height: 1.35; + word-break: break-word; +} +.wpsubs-p1-summary-meta { + display: flex; + align-items: center; + gap: 6px; + padding-top: 12px; + border-top: 1px dashed var(--wpsubs-border); + font-size: 12px; + color: var(--wpsubs-text-muted); +} +.wpsubs-p1-summary-meta svg { + color: var(--wpsubs-text-subtle); + flex-shrink: 0; +} + +/* Button loading state (plan create / connect requests in flight). */ +.wpsubs-btn.is-loading { + opacity: 0.6; + cursor: progress; + pointer-events: none; +} + +/* Free-trial line in the step-2 plan preview. */ +.wpsubs-p1-preview-trial { + margin: 6px 0 0; + font-size: 12px; + font-weight: 600; + color: #15803d; +} + +/* Pro-locked plan-type cards on the create-plan step. */ +.wpsubs-p2-option-card:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +/* List layout for the plan-type cards (create-plan step): each row is + icon + text, full width, with the check centered on the right. */ +.wpsubs-p2-option-cards--list { + display: flex; + flex-direction: column; + gap: 8px; +} +.wpsubs-p2-option-cards--list .wpsubs-p2-option-card { + flex-direction: row; + align-items: center; + gap: 12px; + padding: 12px 44px 12px 16px; +} +.wpsubs-p2-option-cards--list .wpsubs-p2-option-card__icon { + margin-bottom: 0; + flex-shrink: 0; +} +.wpsubs-p2-option-cards--list .wpsubs-p2-option-card__check { + top: 50%; + transform: translateY(-50%); +} +.wpsubs-p2-option-card__text { + min-width: 0; +} diff --git a/assets/js/admin/onboarding-wizard.js b/assets/js/admin/onboarding-wizard.js index 583818dc..66e5b447 100644 --- a/assets/js/admin/onboarding-wizard.js +++ b/assets/js/admin/onboarding-wizard.js @@ -1,146 +1,294 @@ /** - * Onboarding Wizard — SPA-style - * All pages rendered at once; JS controls section visibility - * PHP values passed via localized script data (see Menu::enqueue_admin_assets) + * Onboarding Wizard — SPA-style. + * + * Flow: + * 1. Create Plan — pick a plan type + name (name auto-fills from the type). + * 2. Set Frequency — billing duration (frequency, interval, trial, fee). + * 3. Connect — attach the plan to a new or existing product. + * 4. Finish — summary. + * + * The plan (group), duration (term) and product relation are created through + * the Plans REST API (wpsubscription/v1/plans) once both the plan details and + * the frequency are known (leaving page 2). Creating a brand-new product uses + * an admin-ajax handler. All PHP values arrive via `subscrpt_wizard`. */ (function ($) { "use strict"; + var INTERVAL_TO_INT = { day: 1, week: 2, month: 3, year: 4 }; + var Wizard = { - currentPage: 1, - ajaxUrl: "", - subscriptionsUrl: "", + cfg: {}, + autoName: "", + // Created leaving page 2, used on page 3. + groupId: 0, + termId: 0, + planTitle: "", + billingText: "", init: function () { - this.ajaxUrl = subscrpt_wizard.ajax_url; - this.subscriptionsUrl = subscrpt_wizard.subscriptions_url; + this.cfg = window.subscrpt_wizard || {}; + this.hasProducts = $("#subscrpt-has-products").val() === "1"; + this.autoName = $.trim($("#subscrpt_plan_title").val()); this.bindEvents(); - this.showRelevantProductSection(); - this.initPageIndicator(); this.initLivePreview(); - this.initStepperState(); + this.updatePlanSummary(); + $("#subscrpt-link-plans").attr("href", this.cfg.plans_url || "#"); + $("#subscrpt-link-products").attr("href", this.cfg.products_url || "#"); }, - initPageIndicator: function () { - this.currentPage = parseInt($("#subscrpt-current-page").text(), 10) || 1; - }, + // ----- REST helper ----- - initStepperState: function () { - var page = parseInt($("#subscrpt-wizard-page").val(), 10) || 1; - if (page > 1) { - $("#subscrpt-stepper").show(); - $(".wpsubs-wizard-stepper__step").removeClass("active done"); - $(".wpsubs-wizard-stepper__step").each(function () { - var step = parseInt($(this).data("step"), 10); - if (step < page) { - $(this).addClass("done"); - } else if (step === page) { - $(this).addClass("active"); + api: function (method, path, body) { + return fetch(this.cfg.rest_url + path, { + method: method, + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + "X-WP-Nonce": this.cfg.rest_nonce || "", + }, + body: body ? JSON.stringify(body) : undefined, + }).then(function (res) { + return res.json().then(function (data) { + if (!res.ok) { + throw new Error((data && data.message) || "Request failed."); } + return data; }); - } + }); }, + // ----- Events ----- + bindEvents: function () { - // Page 1 - $(document).on("click", "#subscrpt-btn-start", $.proxy(this.goToPage2, this)); + // Page 1 (plan). + $(document).on("click", ".wpsubs-plan-type-card", $.proxy(this.selectPlanType, this)); $(document).on("click", "#subscrpt-btn-skip", $.proxy(this.skip, this)); - - // Page 2: product toggle - $(document).on("click", "#subscrpt-btn-create-new", $.proxy(this.showNewProduct, this)); - $(document).on("click", "#subscrpt-btn-use-existing", $.proxy(this.showExistingProduct, this)); - - // Page 2: product search - $(document).on("focus", "#subscrpt-product-search-input", $.proxy(this.openProductSearch, this)); - $(document).on("input", "#subscrpt-product-search-input", $.proxy(this.filterProducts, this)); - $(document).on("click", ".wpsubs-p2-product-search__item", $.proxy(this.onProductItemClick, this)); + $(document).on("click", "#subscrpt-btn-next-1", $.proxy(this.nextFromPlan, this)); + + // Page 2 (frequency). + $(document).on("click", "#subscrpt-btn-back-1", $.proxy(this.goToPage, this, 1)); + $(document).on("click", "#subscrpt-btn-create-plan", $.proxy(this.createPlan, this)); + + // Page 3 (product). + $(document).on("click", "#subscrpt-btn-back-2", $.proxy(this.goToPage, this, 2)); + $(document).on("click", ".wpsubs-connect-mode-card", $.proxy(this.selectConnectMode, this)); + $(document).on("click", "#subscrpt-btn-connect", $.proxy(this.connect, this)); + $(document).on("focus", "#subscrpt-product-search-input", this.openProductSearch); + $(document).on("input", "#subscrpt-product-search-input", this.filterProducts); + $(document).on("click", ".wpsubs-p2-product-search__item", $.proxy(this.onProductPick, this)); + $(document).on("click", "#subscrpt-btn-clear-product", $.proxy(this.clearProduct, this)); $(document).on("click", function (e) { if (!$(e.target).closest(".wpsubs-p2-product-search").length) { $("#subscrpt-product-search-dropdown").hide(); } }); - $(document).on("click", "#subscrpt-btn-clear-product", $.proxy(this.clearProductSelection, this)); - $(document).on("click", ".wpsubs-p2-variation-item", $.proxy(this.onVariationSelect, this)); - - // Page 2: navigation - $(document).on("click", "#subscrpt-btn-back", $.proxy(this.goToPage1, this)); - $(document).on("click", "#subscrpt-btn-save", $.proxy(this.savePage2, this)); - // Page 3 + // Page 4 (finish). $(document).on("click", "#subscrpt-btn-add-another", $.proxy(this.restart, this)); - $(document).on("click", "#subscrpt-btn-start-over", $.proxy(this.restart, this)); }, - // ----- Page transitions ----- + // ----- Navigation ----- - goToPage1: function () { - this.switchSection(1); + goToPage: function (pageNum, e) { + if (e && e.preventDefault) { + e.preventDefault(); + } + this.switchSection(pageNum); }, - goToPage2: function () { - this.switchSection(2); + switchSection: function (pageNum) { + $("#subscrpt-wizard-page").val(pageNum); + + $(".wpsubs-wizard-stepper__step").removeClass("active done"); + $(".wpsubs-wizard-stepper__step").each(function () { + var step = parseInt($(this).data("step"), 10); + if (step < pageNum) { + $(this).addClass("done"); + } else if (step === pageNum) { + $(this).addClass("active"); + } + }); + + $(".wpsubs-wizard-section").removeClass("active"); + $("#subscrpt-section-" + pageNum).addClass("active"); + $("html, body").animate({ scrollTop: 0 }, 150); }, - goToPage3: function (productId) { - $("#subscrpt-product-id").val(productId || 0); - this.switchSection(3); + skip: function (e) { + e.preventDefault(); + window.location.href = this.cfg.subscriptions_url; }, - switchSection: function (pageNum) { - this.currentPage = pageNum; + // ----- Page 1: plan type + name ----- - $("#subscrpt-wizard-page").val(pageNum); + selectPlanType: function (e) { + var card = $(e.currentTarget); + $(".wpsubs-plan-type-card").removeClass("active"); + card.addClass("active"); + $("#subscrpt-plan-type").val(card.data("type")); - if (pageNum === 1) { - $("#subscrpt-stepper").hide(); - } else { - $("#subscrpt-stepper").show(); - $(".wpsubs-wizard-stepper__step").removeClass("active done"); - $(".wpsubs-wizard-stepper__step").each(function () { - var step = parseInt($(this).data("step"), 10); - if (step < pageNum) { - $(this).addClass("done"); - } - }); - $('.wpsubs-wizard-stepper__step[data-step="' + pageNum + '"]').addClass("active"); + // Auto-fill the name, but never clobber a name the user has edited. + var suggested = card.data("name") ? String(card.data("name")) : ""; + var current = $.trim($("#subscrpt_plan_title").val()); + if (!current || current === this.autoName) { + $("#subscrpt_plan_title").val(suggested); } + this.autoName = suggested; - $(".wpsubs-wizard-section").removeClass("active"); - $("#subscrpt-section-" + pageNum).addClass("active"); + $("#p1-summary-type").text(card.data("label") ? String(card.data("label")) : ""); + this.updatePlanSummary(); }, - // ----- Page 1 actions ----- + updatePlanSummary: function () { + $("#p1-summary-name").text($.trim($("#subscrpt_plan_title").val()) || "Your plan"); + }, - skip: function (e) { + nextFromPlan: function (e) { e.preventDefault(); - window.location.href = this.subscriptionsUrl; + if (!$.trim($("#subscrpt_plan_title").val())) { + window.alert("Please enter a plan name."); + return; + } + this.switchSection(2); }, - // ----- Page 2 actions ----- + // ----- Page 2: live preview + create plan ----- - showNewProduct: function (e) { - e.preventDefault(); - $(".wpsubs-p2-option-card, .product-toggle-btn").removeClass("active"); - $(e.currentTarget).addClass("active"); - $("#subscrpt-existing-product-fields").hide(); - // Restore name field editability - $("#subscrpt_product_name").prop("readonly", false).val(""); - $("#subscrpt_product_price").val(""); - $("#subscrpt-btn-save").html("Create product ›"); - this.updatePreview(); + initLivePreview: function () { + var self = this; + $(document).on("input", "#subscrpt_billing_frequency, #subscrpt_free_trial", function () { + self.updatePlanPreview(); + }); + $(document).on("input", "#subscrpt_plan_title", function () { + self.updatePlanPreview(); + self.updatePlanSummary(); + }); + $(document).on( + "wpsubs:select", + "#subscrpt-billing-interval-select, #subscrpt-trial-interval-select", + function () { + self.updatePlanPreview(); + }, + ); + }, + + unitLabel: function (unit, count) { + var n = parseInt(count, 10) || 1; + return n > 1 ? n + " " + unit + "s" : unit; }, - showExistingProduct: function (e) { + updatePlanPreview: function () { + var title = $("#subscrpt_plan_title").val() || "Your plan"; + var freq = $("#subscrpt_billing_frequency").val() || "1"; + var interval = $("input[name='subscrpt_billing_interval']").val() || "month"; + var trial = $("#subscrpt_free_trial").val(); + var trialInterval = $("input[name='subscrpt_trial_interval']").val() || "day"; + + $("#p2-preview-plan").text(title); + $("#p2-preview-billing").text("every " + this.unitLabel(interval, freq)); + + var trialNum = parseInt(trial, 10); + if (trialNum > 0) { + $("#p2-preview-trial") + .text(trialNum + " " + (trialNum > 1 ? trialInterval + "s" : trialInterval) + " free trial") + .show(); + } else { + $("#p2-preview-trial").hide(); + } + }, + + createPlan: function (e) { e.preventDefault(); - $(".wpsubs-p2-option-card, .product-toggle-btn").removeClass("active"); - $(e.currentTarget).addClass("active"); - $("#subscrpt-existing-product-fields").show(); - $("#subscrpt-btn-save").html("Update product ›"); - // If already has a selection, re-show chip - var selectedVal = $("#subscrpt_existing_product").val(); - if (selectedVal) { - this.showProductChip($("#subscrpt_existing_product option:selected")); + + var self = this; + var $btn = $("#subscrpt-btn-create-plan"); + var title = $.trim($("#subscrpt_plan_title").val()); + var type = $("#subscrpt-plan-type").val() || "recurring"; + + if (!title) { + window.alert("Please enter a plan name."); + return; + } + + var freq = parseInt($("#subscrpt_billing_frequency").val(), 10) || 1; + var interval = $("input[name='subscrpt_billing_interval']").val() || "month"; + var trial = $.trim($("#subscrpt_free_trial").val()); + var trialInterval = $("input[name='subscrpt_trial_interval']").val() || "day"; + var signupFee = this.cfg.is_pro ? $.trim($("#subscrpt_signup_fee").val()) : ""; + + if ($btn.hasClass("is-loading")) { + return; + } + $btn.addClass("is-loading").prop("disabled", true); + + var termBody = { + type: type, + title: title, + billing_frequency: freq, + billing_interval: INTERVAL_TO_INT[interval] || 3, + billing_length: 0, + free_trial: trial || "", + signup_fee: { amount: signupFee || "" }, + status: "active", + data: { free_trial_interval: trialInterval }, + }; + + // 1) Create the plan group. 2) Reuse the auto-seeded draft term (or create + // one) with the chosen duration. + this.api("POST", "/groups", { + title: title, + type: type, + product_type: 1, + status: "active", + }) + .then(function (group) { + self.groupId = group.id; + self.planTitle = title; + self.billingText = "every " + self.unitLabel(interval, freq); + termBody.plan_group_id = group.id; + + var seeded = group.plans && group.plans.length ? group.plans[0] : null; + if (seeded && seeded.id) { + return self.api("PUT", "/terms/" + seeded.id, termBody).then(function () { + return seeded.id; + }); + } + return self.api("POST", "/terms", termBody).then(function (term) { + return term.id; + }); + }) + .then(function (termId) { + self.termId = termId; + $btn.removeClass("is-loading").prop("disabled", false); + self.switchSection(3); + }) + .catch(function (err) { + $btn.removeClass("is-loading").prop("disabled", false); + window.alert(err.message || "Could not create the plan. Please try again."); + }); + }, + + // ----- Page 3: connect to a product ----- + + // Which connect mode is active. Without existing products the only option + // is creating a new one. + currentConnectMode: function () { + if (!this.hasProducts) { + return "new"; } + var active = $(".wpsubs-connect-mode-card.active"); + return active.length ? active.data("mode") : "existing"; + }, + + selectConnectMode: function (e) { + var card = $(e.currentTarget); + var mode = card.data("mode"); + $(".wpsubs-connect-mode-card").removeClass("active"); + card.addClass("active"); + $("#subscrpt-connect-existing").toggle(mode === "existing"); + $("#subscrpt-connect-new").toggle(mode === "new"); + $("#subscrpt-btn-connect").html((mode === "new" ? "Create & connect" : "Connect plan") + " ›"); }, openProductSearch: function () { @@ -154,7 +302,7 @@ $("#subscrpt-product-search-dropdown").show(); var visible = 0; $(".wpsubs-p2-product-search__item").each(function () { - var name = $(this).data("name") ? $(this).data("name").toLowerCase() : ""; + var name = $(this).data("name") ? String($(this).data("name")).toLowerCase() : ""; var sku = $(this).data("sku") ? String($(this).data("sku")).toLowerCase() : ""; if (!q || name.indexOf(q) >= 0 || sku.indexOf(q) >= 0) { $(this).show(); @@ -166,200 +314,17 @@ $(".wpsubs-p2-product-search__empty").toggle(visible === 0); }, - onProductItemClick: function (e) { + onProductPick: function (e) { var item = $(e.currentTarget); - if (item.hasClass("wpsubs-p2-product-search__item--locked")) { - return; - } var id = String(item.data("id")); var name = item.data("name") || ""; var price = item.data("price") != null ? String(item.data("price")) : ""; var type = item.data("type") || ""; var sku = item.data("sku") ? String(item.data("sku")) : ""; - var productType = item.data("product-type") || ""; $("#subscrpt-existing-product-hidden").val(id); $("#subscrpt-product-search-dropdown").hide(); - this.showProductChip(name, price, type, sku); - $("#subscrpt_product_name").val(name); - $("#subscrpt_product_price").val(price); - - // Reset variation picker state - $("#subscrpt-variation-id-hidden").val(""); - - if (productType === "variable") { - // Variation picker handles subscription field autofill - this.fetchAndShowVariations(id); - } else { - $("#subscrpt-variation-picker-wrap").hide(); - - // Autofill subscription fields from product meta - var billingPeriod = item.data("billing-period"); - var billingPer = item.data("billing-per"); - var trialPer = item.data("trial-per"); - var signupFee = item.data("signup-fee"); - - if (billingPeriod) { - this.setAdvSelectValue("#subscrpt-billing-period-select", String(billingPeriod)); - } - if (billingPer) { - $("#subscrpt_billing_per").val(billingPer); - $("#subscrpt_billing_per_visible").val(billingPer); - } - if (trialPer !== undefined && trialPer !== "") { - $("#subscrpt_trial_timing_per").val(trialPer); - } - if (signupFee !== undefined && signupFee !== "") { - $("#subscrpt_signup_fee").val(signupFee); - } - } - - this.updatePreview(); - }, - - fetchAndShowVariations: function (productId) { - var self = this; - var wrap = $("#subscrpt-variation-picker-wrap"); - var list = $("#subscrpt-variation-picker-list"); - - wrap.show(); - list.html( - '

' + - (window.subscrpt_wizard_i18n ? subscrpt_wizard_i18n.loading : "Loading variations…") + - "

", - ); - - $.post( - this.ajaxUrl, - { - action: "subscrpt_get_product_variations", - nonce: $("#subscrpt_wizard_nonce").val(), - product_id: productId, - }, - function (response) { - if (response.success && response.data && response.data.length) { - self.renderVariationPicker(response.data); - } else { - wrap.hide(); - } - }, - ).fail(function () { - wrap.hide(); - }); - }, - - renderVariationPicker: function (variations) { - var self = this; - var symbol = (window.subscrpt_wizard && subscrpt_wizard.currency_symbol) || "$"; - var html = '

Select a variation

'; - html += '
'; - - variations.forEach(function (v) { - var meta = v.sku ? "SKU " + v.sku : ""; - var priceDisplay = - v.price !== "" && v.price !== null && v.price !== undefined ? symbol + parseFloat(v.price).toFixed(2) : ""; - - html += - '
' + - '
' + - '
' + - '

' + - self.escHtml(v.label) + - "

" + - (meta ? '

' + self.escHtml(meta) + "

" : "") + - "
" + - (priceDisplay - ? '' + self.escHtml(priceDisplay) + "" - : "") + - "
"; - }); - html += "
"; - $("#subscrpt-variation-picker-list").html(html); - $("#subscrpt-variation-picker-wrap").show(); - }, - - onVariationSelect: function (e) { - var item = $(e.currentTarget); - $(".wpsubs-p2-variation-item").removeClass("selected"); - item.addClass("selected"); - - $("#subscrpt-variation-id-hidden").val(String(item.data("id"))); - - // Autofill subscription fields and price from variation meta - var price = item.data("price"); - var billingPeriod = item.data("billing-period"); - var billingPer = item.data("billing-per"); - var trialPer = item.data("trial-per"); - var signupFee = item.data("signup-fee"); - - if (price !== undefined && price !== "") { - $("#subscrpt_product_price").val(price); - } - if (billingPeriod) { - this.setAdvSelectValue("#subscrpt-billing-period-select", String(billingPeriod)); - } - if (billingPer) { - $("#subscrpt_billing_per").val(billingPer); - $("#subscrpt_billing_per_visible").val(billingPer); - } - if (trialPer !== undefined && trialPer !== "") { - $("#subscrpt_trial_timing_per").val(trialPer); - } - if (signupFee !== undefined && signupFee !== "") { - $("#subscrpt_signup_fee").val(signupFee); - } - - this.updatePreview(); - }, - - escHtml: function (str) { - return String(str).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); - }, - - escAttr: function (str) { - return String(str) - .replace(/&/g, "&") - .replace(/"/g, """) - .replace(/'/g, "'") - .replace(//g, ">"); - }, - - setAdvSelectValue: function (selector, value) { - var el = $(selector); - if (!el.length) return; - var menuItem = el.find('.wpsubs-adv-select__item[data-value="' + value + '"]'); - var label = menuItem.length ? menuItem.find(".wpsubs-adv-select__item-label").text().trim() : value; - el.find('input[type="hidden"]').val(value); - el.find(".wpsubs-adv-select__label").text(label); - }, - - showProductChip: function (name, price, type, sku) { var initials = name .split(" ") .filter(Boolean) @@ -368,206 +333,147 @@ return w[0].toUpperCase(); }) .join(""); + var meta = [sku ? "SKU " + sku : null, type].filter(Boolean).join(" · "); - var meta = [sku ? "SKU " + sku : null, type, price ? "$" + parseFloat(price).toFixed(2) : null] - .filter(Boolean) - .join(" · "); - - $("#p2-chip-avatar").text(initials); - $("#p2-chip-name").text(name); - $("#p2-chip-meta").text(meta); + $("#p3-chip-avatar").text(initials || "?"); + $("#p3-chip-name").text(name); + $("#p3-chip-meta").text(meta); $("#subscrpt-product-select-wrap").hide(); $("#subscrpt-selected-product-chip").show(); + + if (price && !$.trim($("#subscrpt_connect_price").val())) { + $("#subscrpt_connect_price").val(parseFloat(price).toFixed(2)); + } + + this.selectedProductName = name; }, - clearProductSelection: function () { + clearProduct: function () { $("#subscrpt-existing-product-hidden").val(""); $("#subscrpt-product-search-input").val(""); $(".wpsubs-p2-product-search__item").show(); - $(".wpsubs-p2-product-search__empty").hide(); - $("#subscrpt-product-search-dropdown").hide(); $("#subscrpt-selected-product-chip").hide(); $("#subscrpt-product-select-wrap").show(); - $("#subscrpt_product_name").val(""); - $("#subscrpt_product_price").val(""); - // Reset variation picker - $("#subscrpt-variation-picker-wrap").hide(); - $("#subscrpt-variation-picker-list").empty(); - $("#subscrpt-variation-id-hidden").val(""); - this.updatePreview(); + this.selectedProductName = ""; }, - showRelevantProductSection: function () { - var activeBtn = $(".product-toggle-btn.active"); - var mode = activeBtn.length ? activeBtn.data("mode") : "new"; - - if (mode === "existing") { - $("#subscrpt-existing-product-fields").show(); - var selectedId = $("#subscrpt-existing-product-hidden").val(); - if (selectedId) { - var item = $(".wpsubs-p2-product-search__item[data-id='" + selectedId + "']"); - if (item.length) { - var name = item.data("name") || ""; - var price = item.data("price") != null ? String(item.data("price")) : ""; - var type = item.data("type") || ""; - var sku = item.data("sku") ? String(item.data("sku")) : ""; - this.showProductChip(name, price, type, sku); - } - } - } else { - $("#subscrpt-existing-product-fields").hide(); - } - }, - - // ----- Live preview ----- + connect: function (e) { + e.preventDefault(); - initLivePreview: function () { var self = this; - $(document).on("input", "#subscrpt_product_name", function () { - self.updatePreview(); - }); - $(document).on("input", "#subscrpt_product_price", function () { - self.updatePreview(); - }); - $(document).on("wpsubs:select", "#subscrpt-billing-period-select", function () { - self.updatePreview(); - }); - }, - - updatePreview: function () { - var name = $("#subscrpt_product_name").val() || "Your product"; - var price = $("#subscrpt_product_price").val() || "0.00"; - var period = $("input[name='subscrpt_billing_period']").val() || "months"; - - $("#p2-preview-name").text(name); - $("#p2-preview-price").text(parseFloat(price).toFixed(2)); - $("#p2-preview-period").text(period); - }, - - // ----- Save & validate ----- + var $btn = $("#subscrpt-btn-connect"); + var price = $.trim($("#subscrpt_connect_price").val()); - savePage2: function (e) { - e.preventDefault(); - - if (!this.validatePage2()) { + if (price && (isNaN(parseFloat(price)) || parseFloat(price) < 0)) { + window.alert("Please enter a valid price."); return; } - var productMode = $(".product-toggle-btn.active").data("mode") || "new"; - var confirmMsg = - productMode === "new" - ? "Create this subscription product?" - : "Apply these subscription settings to the selected product?"; - - if (!confirm(confirmMsg)) { + if ($btn.hasClass("is-loading")) { return; } - var data = { - action: "subscrpt_save_wizard_page2", - nonce: $("#subscrpt_wizard_nonce").val(), - product_mode: $(".product-toggle-btn.active").data("mode") || "new", - product_name: $("#subscrpt_product_name").val(), - product_price: $("#subscrpt_product_price").val(), - existing_product_id: $("#subscrpt-existing-product-hidden").val(), - variation_id: $("#subscrpt-variation-id-hidden").val(), - timing_option: $("#subscrpt_timing_option").val(), - billing_per: $("#subscrpt_billing_per").val(), - billing_period: $("input[name='subscrpt_billing_period']").val(), - trial_timing_per: $("#subscrpt_trial_timing_per").val(), - signup_fee: $("#subscrpt_signup_fee").val(), - // hidden compat fields - trial_enabled: 0, - trial_timing_option: $("input[name='subscrpt_trial_timing_option']").val() || "days", - length_enabled: 0, - length_per: "", - length_option: "months", - }; - - var self = this; - - $.post(this.ajaxUrl, data, function (response) { - if (response.success) { - window.location.reload(); - } else { - alert(response.data.message || "Something went wrong. Please try again."); - } - }).fail(function () { - alert("Server error. Please try again."); - }); - }, - - validatePage2: function () { - var isValid = true; - var messages = []; - var productMode = $(".product-toggle-btn.active").data("mode") || "new"; - var productName = $("#subscrpt_product_name").val().trim(); - var price = $("#subscrpt_product_price").val().trim(); - var existing = $("#subscrpt-existing-product-hidden").val(); - var billing = $("input[name='subscrpt_billing_period']").val(); - - if (productMode === "new") { - if (!productName) { - messages.push("Product name is required."); - isValid = false; + if (this.currentConnectMode() === "existing") { + var productId = $("#subscrpt-existing-product-hidden").val(); + if (!productId) { + window.alert("Please select a product."); + return; } + $btn.addClass("is-loading").prop("disabled", true); + this.createRelation(productId, this.selectedProductName || "Product", price, $btn); } else { - if (!existing) { - messages.push("Please select an existing product."); - isValid = false; - } - if ( - existing && - $("#subscrpt-variation-picker-wrap").is(":visible") && - !$("#subscrpt-variation-id-hidden").val() - ) { - messages.push("Please select a variation."); - isValid = false; + var name = $.trim($("#subscrpt_new_product_name").val()); + if (!name) { + window.alert("Please enter a product name."); + return; } + $btn.addClass("is-loading").prop("disabled", true); + // Create the product first, then connect the plan to it. + $.post( + this.cfg.ajax_url, + { + action: "subscrpt_create_wizard_product", + nonce: $("#subscrpt_wizard_nonce").val(), + product_name: name, + product_price: price, + }, + function (response) { + if (response && response.success) { + self.createRelation(response.data.product_id, name, price, $btn); + } else { + $btn.removeClass("is-loading").prop("disabled", false); + window.alert((response && response.data && response.data.message) || "Could not create the product."); + } + }, + ).fail(function () { + $btn.removeClass("is-loading").prop("disabled", false); + window.alert("Server error. Please try again."); + }); } + }, - if (!price || isNaN(parseFloat(price)) || parseFloat(price) < 0) { - messages.push("Please enter a valid price."); - isValid = false; - } + createRelation: function (productId, productName, price, $btn) { + var self = this; + this.api("POST", "/relations", { + plan_id: this.termId, + oid: parseInt(productId, 10), + vid: 0, + type: 1, + status: "active", + exclude: false, + data: { regular_price: price, sale_price: "", discount_value: 0 }, + }) + .then(function () { + $btn.removeClass("is-loading").prop("disabled", false); + self.showDone(productName, price); + }) + .catch(function (err) { + $btn.removeClass("is-loading").prop("disabled", false); + window.alert(err.message || "Could not connect the plan. Please try again."); + }); + }, - if (!billing) { - messages.push("Please select a billing period."); - isValid = false; - } + // ----- Page 4: finish ----- - if (!isValid) { - alert(messages.join("\n")); - } + showDone: function (productName, price) { + var symbol = this.cfg.currency_symbol || "$"; + var planInitials = (this.planTitle || "Plan") + .split(" ") + .filter(Boolean) + .slice(0, 2) + .map(function (w) { + return w[0].toUpperCase(); + }) + .join(""); - return isValid; - }, + $("#p4-plan-avatar").text(planInitials || "P"); + $("#p4-plan-name").text(this.planTitle || "Plan"); + $("#p4-plan-billing").text(this.billingText || ""); + $("#p4-product-name").text(productName); + $("#p4-price").text(price ? symbol + parseFloat(price).toFixed(2) : "—"); - // ----- Page 3 actions ----- + this.switchSection(4); + }, restart: function (e) { e.preventDefault(); var self = this; $.post( - this.ajaxUrl, + this.cfg.ajax_url, { action: "subscrpt_reset_wizard", nonce: $("#subscrpt_wizard_nonce").val(), }, function () { + self.groupId = 0; + self.termId = 0; + self.planTitle = ""; + self.billingText = ""; + $("#subscrpt_free_trial, #subscrpt_signup_fee, #subscrpt_connect_price").val(""); + $("#subscrpt_billing_frequency").val("1"); + $("#subscrpt_new_product_name").val(""); + self.clearProduct(); self.switchSection(1); - $("#subscrpt-current-page").text("1"); - $("#subscrpt_product_name").val(""); - $("#subscrpt_product_price").val(""); - $("#subscrpt_timing_option").val("never"); - $("#subscrpt_billing_per").val("1"); - $("input[name='subscrpt_billing_period']").val("months"); - $("#subscrpt_trial_timing_per").val("0"); - $("#subscrpt_signup_fee").val(""); - // Reset chip - $("#subscrpt-selected-product-chip").hide(); - $("#subscrpt-product-select-wrap").show(); - $("#subscrpt_existing_product").val(""); }, ); }, diff --git a/includes/Admin/Menu.php b/includes/Admin/Menu.php index 3372a0c0..19710590 100644 --- a/includes/Admin/Menu.php +++ b/includes/Admin/Menu.php @@ -62,13 +62,27 @@ public function enqueue_admin_assets() { SUBSCRPT_VERSION, true ); + $subscrpt_wizard_has_products = (bool) wc_get_products( + array( + 'status' => array( 'publish', 'draft', 'pending', 'private' ), + 'limit' => 1, + 'return' => 'ids', + ) + ); + wp_localize_script( 'subscrpt-onboarding-wizard', 'subscrpt_wizard', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'subscriptions_url' => admin_url( 'admin.php?page=wp-subscription' ), + 'products_url' => admin_url( 'edit.php?post_type=product' ), + 'plans_url' => admin_url( 'admin.php?page=wp-subscription-plans' ), + 'rest_url' => rest_url( 'wpsubscription/v1/plans' ), + 'rest_nonce' => wp_create_nonce( 'wp_rest' ), 'currency_symbol' => get_woocommerce_currency_symbol(), + 'is_pro' => subscrpt_pro_activated(), + 'has_products' => $subscrpt_wizard_has_products, ) ); @@ -782,15 +796,12 @@ public function render_support_page() { * Initial load always shows page 1 (JS handles transitions from there) */ public function render_onboarding_wizard() { - // Start session if not already started + // Start session if not already started (used by the wizard reset handler). if ( ! session_id() && ! headers_sent() ) { session_start(); } - // Always start at page 1 on direct load (SPA behavior — JS drives page transitions) - $GLOBALS['wizard_page'] = 1; - - $this->render_admin_header( __( 'Setup Wizard', 'subscription' ), __( 'Create your first subscription product', 'subscription' ) ); + $this->render_admin_header( __( 'Setup Wizard', 'subscription' ), __( 'Create your first subscription plan', 'subscription' ) ); include __DIR__ . '/views/onboarding-wizard.php'; $this->render_admin_footer(); } diff --git a/includes/Admin/OnboardingAjax.php b/includes/Admin/OnboardingAjax.php index 3377b595..b8013c29 100644 --- a/includes/Admin/OnboardingAjax.php +++ b/includes/Admin/OnboardingAjax.php @@ -5,7 +5,10 @@ /** * AJAX handlers for the onboarding wizard. * - * Handles page-2 save (product create/update), variation fetch, and wizard reset. + * The wizard creates a plan + duration and connects it to a product entirely + * through the Plans REST API (wpsubscription/v1/plans). The one thing REST does + * not offer is creating a WooCommerce product, so that single step lives here; + * everything else — group, term, relation — is done client-side against REST. * * @package SpringDevs\Subscription\Admin */ @@ -15,200 +18,54 @@ class OnboardingAjax { * Initialize the class. */ public function __construct() { - add_action( 'wp_ajax_subscrpt_save_wizard_page2', array( $this, 'save_wizard_page2' ) ); + add_action( 'wp_ajax_subscrpt_create_wizard_product', array( $this, 'create_wizard_product' ) ); add_action( 'wp_ajax_subscrpt_reset_wizard', array( $this, 'reset_wizard' ) ); - add_action( 'wp_ajax_subscrpt_get_product_variations', array( $this, 'get_product_variations' ) ); } /** - * Save wizard Page 2 data to session and create/update product. + * Create a bare simple product (name + price) for the wizard to connect a + * plan to. No subscription meta is written here — connecting the plan (the + * REST relation) is what makes the product subscribable. * * @return void Sends JSON. */ - public function save_wizard_page2() { + public function create_wizard_product() { check_ajax_referer( 'subscrpt_onboarding_wizard', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'subscription' ) ), 403 ); } - if ( ! session_id() ) { - session_start(); - } - - $product_mode = isset( $_POST['product_mode'] ) ? sanitize_text_field( wp_unslash( $_POST['product_mode'] ) ) : 'new'; - $product_name = isset( $_POST['product_name'] ) ? sanitize_text_field( wp_unslash( $_POST['product_name'] ) ) : ''; - $product_price = isset( $_POST['product_price'] ) ? sanitize_text_field( wp_unslash( $_POST['product_price'] ) ) : ''; - $existing_product = isset( $_POST['existing_product_id'] ) ? absint( $_POST['existing_product_id'] ) : 0; - $variation_id = isset( $_POST['variation_id'] ) ? absint( $_POST['variation_id'] ) : 0; - $timing_option = isset( $_POST['timing_option'] ) ? sanitize_text_field( wp_unslash( $_POST['timing_option'] ) ) : 'never'; - $billing_per = isset( $_POST['billing_per'] ) ? absint( $_POST['billing_per'] ) : 1; - $billing_period = isset( $_POST['billing_period'] ) ? sanitize_text_field( wp_unslash( $_POST['billing_period'] ) ) : 'months'; - $trial_timing_per = isset( $_POST['trial_timing_per'] ) ? absint( $_POST['trial_timing_per'] ) : 0; - $signup_fee = isset( $_POST['signup_fee'] ) ? sanitize_text_field( wp_unslash( $_POST['signup_fee'] ) ) : ''; - $trial_enabled = isset( $_POST['trial_enabled'] ) ? (int) $_POST['trial_enabled'] : 0; - $trial_timing_opt = isset( $_POST['trial_timing_option'] ) ? sanitize_text_field( wp_unslash( $_POST['trial_timing_option'] ) ) : 'days'; - $length_enabled = isset( $_POST['length_enabled'] ) ? (int) $_POST['length_enabled'] : 0; - $length_per = isset( $_POST['length_per'] ) ? absint( $_POST['length_per'] ) : 0; - $length_option = isset( $_POST['length_option'] ) ? sanitize_text_field( wp_unslash( $_POST['length_option'] ) ) : 'months'; - - $result_product_id = 0; - - if ( 'new' === $product_mode ) { - $wc_product = new \WC_Product_Simple(); - $wc_product->set_name( $product_name ); - $wc_product->set_regular_price( wc_format_decimal( $product_price ) ); - $wc_product->set_status( 'publish' ); - $this->apply_subscription_meta( $wc_product, $billing_period, $billing_per, $trial_timing_per, $trial_timing_opt, $signup_fee ); - $result_product_id = $wc_product->save(); - - if ( ! $result_product_id ) { - wp_send_json_error( array( 'message' => __( 'Failed to create product. Please try again.', 'subscription' ) ) ); - } - } elseif ( 'existing' === $product_mode && $existing_product > 0 ) { - $parent = wc_get_product( $existing_product ); - - if ( ! $parent ) { - wp_send_json_error( array( 'message' => __( 'Product not found.', 'subscription' ) ) ); - } - - if ( $variation_id > 0 ) { - // Variable product: update parent name only; apply meta + price on the variation. - $parent->set_name( $product_name ); - $parent->save(); - - $variation = wc_get_product( $variation_id ); - if ( $variation ) { - $variation->set_regular_price( wc_format_decimal( $product_price ) ); - $this->apply_subscription_meta( $variation, $billing_period, $billing_per, $trial_timing_per, $trial_timing_opt, $signup_fee ); - $variation->save(); - } - } else { - // Simple product: update name, price, and subscription meta in one save. - $parent->set_name( $product_name ); - $parent->set_regular_price( wc_format_decimal( $product_price ) ); - $this->apply_subscription_meta( $parent, $billing_period, $billing_per, $trial_timing_per, $trial_timing_opt, $signup_fee ); - $parent->save(); - } - - $result_product_id = $existing_product; - } - - $_SESSION['subscrpt_onboarding_wizard'] = array( - 'page' => 3, - 'product_id' => $result_product_id, - 'product_mode' => $product_mode, - 'product_name' => $product_name, - 'product_price' => $product_price, - 'existing_product' => $existing_product, - 'variation_id' => $variation_id, - 'timing_option' => $timing_option, - 'billing_per' => $billing_per, - 'billing_period' => $billing_period, - 'trial_timing_per' => $trial_timing_per, - 'signup_fee' => $signup_fee, - 'trial_enabled' => $trial_enabled, - 'trial_timing_option' => $trial_timing_opt, - 'length_enabled' => $length_enabled, - 'length_per' => $length_per, - 'length_option' => $length_option, - ); - - wp_send_json_success( array( 'product_id' => $result_product_id ) ); - } - - /** - * Stage subscription meta on a WC product object via the WC meta layer. - * Does NOT call save() — the caller must save the object after calling this method. - * - * @param \WC_Product $product Product or variation object. - * @param string $billing_period Billing period unit (days/weeks/months/years). - * @param int $billing_per Billing interval count. - * @param int $trial_per Free trial length. - * @param string $trial_option Free trial period unit. - * @param string $signup_fee One-time sign-up fee (pro only). - * - * @return void - */ - private function apply_subscription_meta( $product, $billing_period, $billing_per, $trial_per, $trial_option, $signup_fee ) { - $product->update_meta_data( '_subscrpt_enabled', true ); - $product->update_meta_data( '_subscrpt_timing_option', $billing_period ); - $product->update_meta_data( '_subscrpt_timing_per', $billing_per ); - $product->update_meta_data( '_subscrpt_trial_timing_per', $trial_per ); - $product->update_meta_data( '_subscrpt_trial_timing_option', $trial_option ); - $product->update_meta_data( '_subscrpt_payment_type', 'recurring' ); + $product_name = isset( $_POST['product_name'] ) ? sanitize_text_field( wp_unslash( $_POST['product_name'] ) ) : ''; + $product_price = isset( $_POST['product_price'] ) ? sanitize_text_field( wp_unslash( $_POST['product_price'] ) ) : ''; - if ( function_exists( 'subscrpt_pro_activated' ) && subscrpt_pro_activated() && '' !== $signup_fee ) { - $product->update_meta_data( '_subscrpt_signup_fee', wc_format_decimal( $signup_fee ) ); + if ( '' === trim( $product_name ) ) { + wp_send_json_error( array( 'message' => __( 'Product name is required.', 'subscription' ) ) ); } - } - - /** - * Return available variations for a variable product (wizard use). - * - * @return void Sends JSON. - */ - public function get_product_variations() { - check_ajax_referer( 'subscrpt_onboarding_wizard', 'nonce' ); - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( array( 'message' => __( 'Permission denied.', 'subscription' ) ), 403 ); + $product = new \WC_Product_Simple(); + $product->set_name( $product_name ); + if ( '' !== trim( $product_price ) ) { + $product->set_regular_price( wc_format_decimal( $product_price ) ); } + $product->set_status( 'publish' ); + $product_id = $product->save(); - $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0; if ( ! $product_id ) { - wp_send_json_error( array( 'message' => __( 'Invalid product.', 'subscription' ) ) ); - } - - $product = wc_get_product( $product_id ); - if ( ! $product || 'variable' !== $product->get_type() ) { - wp_send_json_error( array( 'message' => __( 'Not a variable product.', 'subscription' ) ) ); + wp_send_json_error( array( 'message' => __( 'Failed to create product. Please try again.', 'subscription' ) ) ); } - $available = $product->get_available_variations(); - $result = array(); - - foreach ( $available as $variation ) { - $v_id = $variation['variation_id']; - - // Build human-readable attribute label. - $attr_labels = array(); - foreach ( $variation['attributes'] as $attr_key => $attr_value ) { - if ( '' === $attr_value ) { - $attr_labels[] = __( 'Any', 'subscription' ); - continue; - } - $taxonomy = str_replace( 'attribute_', '', $attr_key ); - if ( taxonomy_exists( $taxonomy ) ) { - $term = get_term_by( 'slug', $attr_value, $taxonomy ); - $attr_labels[] = $term ? $term->name : ucfirst( str_replace( '-', ' ', $attr_value ) ); - } else { - $attr_labels[] = ucfirst( str_replace( '-', ' ', $attr_value ) ); - } - } - $label = implode( ' / ', $attr_labels ); - if ( ! $label ) { - /* translators: %d: variation ID */ - $label = sprintf( __( 'Variation #%d', 'subscription' ), $v_id ); - } - - $result[] = array( - 'id' => $v_id, - 'label' => $label, - 'price' => $variation['display_price'], - 'sku' => $variation['sku'], - 'billing_period' => get_post_meta( $v_id, '_subscrpt_timing_option', true ), - 'billing_per' => get_post_meta( $v_id, '_subscrpt_timing_per', true ) ?: 1, - 'trial_per' => get_post_meta( $v_id, '_subscrpt_trial_timing_per', true ), - 'signup_fee' => get_post_meta( $v_id, '_subscrpt_signup_fee', true ), - ); - } - - wp_send_json_success( $result ); + wp_send_json_success( + array( + 'product_id' => $product_id, + 'product_name' => $product_name, + 'product_price' => $product_price, + ) + ); } /** - * Reset wizard session (for "Add another" action). + * Reset wizard session (for "Add another" / "Start over" actions). * * @return void Sends JSON. */ diff --git a/includes/Admin/views/onboarding-wizard.php b/includes/Admin/views/onboarding-wizard.php index 039a04fe..945545c1 100644 --- a/includes/Admin/views/onboarding-wizard.php +++ b/includes/Admin/views/onboarding-wizard.php @@ -1,7 +1,12 @@ Set Frequency (duration) -> + * Connect to a Product -> Finish. The plan (group), duration (term) and product + * relation are created through the Plans REST API; only product creation uses + * admin-ajax. * * @package SpringDevs\Subscription\Admin */ @@ -10,543 +15,480 @@ exit; } -// Get wizard data from session -$session_data = isset( $_SESSION['subscrpt_onboarding_wizard'] ) ? $_SESSION['subscrpt_onboarding_wizard'] : array(); -$wizard_page = isset( $session_data['page'] ) ? (int) $session_data['page'] : 1; -$product_id = isset( $session_data['product_id'] ) ? (int) $session_data['product_id'] : 0; -$subscriptions_url = admin_url( 'admin.php?page=wp-subscription' ); -$is_pro = subscrpt_pro_activated(); - -// Get existing products for dropdown -$args = array( - 'post_type' => 'product', - 'posts_per_page' => -1, - 'post_status' => 'any', - 'orderby' => 'title', - 'order' => 'ASC', +$is_pro = subscrpt_pro_activated(); +$currency_symbol = get_woocommerce_currency_symbol(); + +// Whether the store has any product at all — decides the connect step's shape. +$has_products = (bool) wc_get_products( + array( + 'status' => array( 'publish', 'draft', 'pending', 'private' ), + 'limit' => 1, + 'return' => 'ids', + ) ); -$products = get_posts( $args ); -// Load product if exists -$product = null; -if ( $product_id > 0 ) { - $product = wc_get_product( $product_id ); -} +// Plan types. Only "recurring" is available on the free plugin; the others are +// shown Pro-locked so the choice is visible but not selectable without Pro. +$plan_types = array( + array( + 'key' => 'recurring', + 'label' => __( 'Recurring', 'subscription' ), + 'desc' => __( 'Charge on a repeating schedule.', 'subscription' ), + 'name' => __( 'Recurring Subscription', 'subscription' ), + 'pro' => false, + 'icon' => '', + ), + array( + 'key' => 'subscribe_save', + 'label' => __( 'Subscribe & Save', 'subscription' ), + 'desc' => __( 'Recurring with a discount.', 'subscription' ), + 'name' => __( 'Subscribe & Save', 'subscription' ), + 'pro' => true, + 'icon' => '', + ), + array( + 'key' => 'installments', + 'label' => __( 'Installments', 'subscription' ), + 'desc' => __( 'Split a price into payments.', 'subscription' ), + 'name' => __( 'Installment Plan', 'subscription' ), + 'pro' => true, + 'icon' => '', + ), +); + +$interval_options = array( + array( + 'value' => 'day', + 'label' => __( 'Day', 'subscription' ), + ), + array( + 'value' => 'week', + 'label' => __( 'Week', 'subscription' ), + ), + array( + 'value' => 'month', + 'label' => __( 'Month', 'subscription' ), + ), + array( + 'value' => 'year', + 'label' => __( 'Year', 'subscription' ), + ), +); + +$avatar_palette = array( + array( + 'bg' => '#fde8d8', + 'fg' => '#b85c20', + ), + array( + 'bg' => '#dbeafe', + 'fg' => '#1d4ed8', + ), + array( + 'bg' => '#ede9fe', + 'fg' => '#6d28d9', + ), + array( + 'bg' => '#d1fae5', + 'fg' => '#065f46', + ), + array( + 'bg' => '#fce7f3', + 'fg' => '#9d174d', + ), +); ?>
- -