diff --git a/assets/css/admin/onboarding-wizard.css b/assets/css/admin/onboarding-wizard.css
index bb66cd93..e2e367da 100644
--- a/assets/css/admin/onboarding-wizard.css
+++ b/assets/css/admin/onboarding-wizard.css
@@ -5,12 +5,88 @@
* Enqueued via includes/Admin/Menu.php (handle: subscrpt-onboarding-wizard).
*/
#subscrpt-onboarding-wizard {
- max-width: 680px;
-}
-#subscrpt-onboarding-wizard:has(#subscrpt-section-1.active),
-#subscrpt-onboarding-wizard:has(#subscrpt-section-2.active) {
max-width: 900px;
}
+
+/* Two columns: the steps on the left, a persistent live-preview rail on the
+ right that stays put while the steps swap. */
+/* One card: steps top-left, its nav footer bottom-left, and the preview
+ spanning the full height of the right column. */
+.wpsubs-wizard-layout {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 420px;
+ grid-template-rows: 1fr auto;
+ grid-template-areas:
+ "steps preview"
+ "footer preview";
+ background: var(--wpsubs-surface, #fff);
+ border: 1px solid var(--wpsubs-border, #e5e7eb);
+ border-radius: var(--wpsubs-radius, 8px);
+ box-shadow: var(--wpsubs-shadow, 0 1px 3px rgba(0, 0, 0, 0.08));
+}
+.wpsubs-wizard-steps {
+ grid-area: steps;
+ min-width: 0;
+ padding: 28px 32px;
+}
+.wpsubs-wizard-aside {
+ grid-area: preview;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 28px 32px;
+ border-left: 1px solid var(--wpsubs-border, #e5e7eb);
+}
+.wpsubs-preview {
+ width: 100%;
+}
+/* Footer carries the step's Back / Continue buttons, under the steps only. */
+.wpsubs-wizard-footer {
+ grid-area: footer;
+ border-top: 1px solid var(--wpsubs-border, #e5e7eb);
+ padding: 16px 32px;
+}
+.wpsubs-wizard-nav {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+/* `display: flex` outweighs the bare `hidden` attribute, so restate it. */
+.wpsubs-wizard-nav[hidden] {
+ display: none;
+}
+.wpsubs-preview__label {
+ margin: 0 0 2px;
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--wpsubs-text-muted, #6b7280);
+ text-align: center;
+}
+.wpsubs-preview__caption {
+ margin: 12px 2px 0;
+ font-size: 12px;
+ line-height: 1.5;
+ color: var(--wpsubs-text-muted, #6b7280);
+ text-align: center;
+}
+@media (max-width: 900px) {
+ .wpsubs-wizard-layout {
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-areas:
+ "steps"
+ "preview"
+ "footer";
+ }
+ .wpsubs-wizard-aside {
+ justify-content: flex-start;
+ border-left: none;
+ border-top: 1px solid var(--wpsubs-border, #e5e7eb);
+ }
+}
+
/* Page sections — all rendered, JS toggles visibility */
.wpsubs-wizard-section {
display: none;
@@ -88,11 +164,9 @@
margin: 0 8px;
}
/* Card */
+/* No longer its own box — it is the left half of the shared card. */
.wpsubs-wizard-card {
- background: var(--wpsubs-surface);
- border: 1px solid var(--wpsubs-border);
- border-radius: var(--wpsubs-radius);
- padding: 28px 32px;
+ padding: 0;
margin-bottom: 16px;
}
/* Form rows (shared) */
@@ -163,24 +237,6 @@
border-color: var(--wpsubs-brand);
background: var(--wpsubs-brand-light);
}
-.wpsubs-p2-option-card__check {
- position: absolute;
- top: 10px;
- right: 10px;
- width: 20px;
- height: 20px;
- border-radius: 50%;
- background: var(--wpsubs-brand);
- display: none;
- align-items: center;
- justify-content: center;
- color: var(--wpsubs-surface);
- font-size: 11px;
- font-weight: 700;
-}
-.wpsubs-p2-option-card.active .wpsubs-p2-option-card__check {
- display: flex;
-}
.wpsubs-p2-option-card__icon {
width: 32px;
height: 32px;
@@ -805,16 +861,6 @@
box-sizing: border-box;
}
/* Page 1 specific — two-column hero layout */
-#subscrpt-section-1 {
- max-width: 860px;
-}
-#subscrpt-section-1 .wpsubs-wizard-card {
- padding: 56px 48px;
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 64px;
- align-items: center;
-}
.wpsubs-p1-left {
display: flex;
flex-direction: column;
@@ -1014,11 +1060,230 @@
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-p2-preview-col {
+ position: static;
+ }
+}
+
+/* Page 1 animated plan-structure preview (right column):
+ one Plan -> three Durations -> two Products, joined by flowing lines. */
+.wpsubs-p1-graph {
+ position: relative;
+ /* Fluid: fill the preview column instead of a fixed pixel box, so the nodes
+ scale with the rail rather than needing a wider container. */
+ width: 100%;
+ aspect-ratio: 300 / 420;
+ margin: 10px auto 0;
+}
+.wpsubs-p1-graph__lines {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ overflow: visible;
+}
+.wpsubs-p1-flow {
+ fill: none;
+ stroke: var(--wpsubs-brand, #ff4d00);
+ stroke-width: 1.5;
+ stroke-linecap: round;
+ stroke-dasharray: 3 6;
+ opacity: 0.5;
+ animation: wpsubs-p1-flow 0.9s linear infinite;
+}
+@keyframes wpsubs-p1-flow {
+ to {
+ stroke-dashoffset: -9;
+ }
+}
+
+.wpsubs-p1-node {
+ position: absolute;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ box-sizing: border-box;
+ padding: 10px 12px;
+ background: var(--wpsubs-surface, #fff);
+ border: 1px solid var(--wpsubs-border, #e5e7eb);
+ border-radius: 9px;
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--wpsubs-text, #374151);
+ line-height: 1.2;
+ white-space: nowrap;
+ overflow: hidden;
+ transform-origin: center;
+ transition:
+ transform 0.2s ease,
+ box-shadow 0.2s ease,
+ border-color 0.12s ease;
+}
+.wpsubs-p1-node__icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: 24px;
+ height: 24px;
+ border-radius: 6px;
+ background: var(--wpsubs-surface-muted, #f3f4f6);
+ color: var(--wpsubs-text-muted, #6b7280);
+}
+.wpsubs-p1-node__icon svg {
+ width: 14px;
+ height: 14px;
+}
+.wpsubs-p1-node__label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+/* Two-line node body: title over a muted subtitle. */
+.wpsubs-p1-node__text {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ min-width: 0;
+}
+.wpsubs-p1-node__title {
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.25;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.wpsubs-p1-node__sub {
+ font-size: 10px;
+ font-weight: 500;
+ line-height: 1.3;
+ color: var(--wpsubs-text-muted, #6b7280);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.wpsubs-p1-node--plan .wpsubs-p1-node__sub {
+ color: var(--wpsubs-brand, #ff4d00);
+ opacity: 0.85;
+}
+
+/* Plan node — brand accent, top of the fan-out. Widths are a % of the graph so
+ they scale with the column. */
+.wpsubs-p1-node--plan {
+ width: 58%;
+ border-color: var(--wpsubs-brand, #ff4d00);
+ background: var(--wpsubs-brand-light, #fff1eb);
+ color: var(--wpsubs-brand-dark, #d93f00);
+}
+.wpsubs-p1-node--plan .wpsubs-p1-node__icon {
+ background: var(--wpsubs-brand, #ff4d00);
+ color: #fff;
+}
+/* Durations (x3) — narrower, so tighten padding/icon to keep text room. */
+.wpsubs-p1-node--dur {
+ width: 30%;
+ padding: 8px 9px;
+ gap: 6px;
+}
+.wpsubs-p1-node--dur .wpsubs-p1-node__icon {
+ width: 20px;
+ height: 20px;
+}
+/* Products (x2). */
+.wpsubs-p1-node--prod {
+ width: 44%;
+}
+
+/* Ghost nodes — the "you can add more later" placeholders. */
+.wpsubs-p1-node--ghost {
+ opacity: 0.45;
+ border-style: dashed;
+ box-shadow: none;
+}
+
+/* The group belonging to the current step gets a brand ring. Ghosts never
+ light up — only the node the step actually fills in. */
+.wpsubs-p1-graph[data-active="plan"] .wpsubs-p1-node--plan,
+.wpsubs-p1-graph[data-active="dur"] .wpsubs-p1-node--dur:not(.wpsubs-p1-node--ghost),
+.wpsubs-p1-graph[data-active="prod"] .wpsubs-p1-node--prod:not(.wpsubs-p1-node--ghost) {
+ border-color: var(--wpsubs-brand, #ff4d00);
+ box-shadow: 0 0 0 3px var(--wpsubs-brand-ring, rgba(255, 77, 0, 0.22));
+ animation: wpsubs-p1-pulse 1.6s ease-in-out infinite;
+}
+@keyframes wpsubs-p1-pulse {
+ 0%,
+ 100% {
+ box-shadow: 0 0 0 3px var(--wpsubs-brand-ring, rgba(255, 77, 0, 0.22));
}
- .wpsubs-p1-right {
- display: none;
+ 50% {
+ box-shadow: 0 0 0 5px rgba(255, 77, 0, 0.1);
+ }
+}
+
+/* Zoom the card whose field is currently focused (wins over the pulse). */
+.wpsubs-p1-graph[data-focus="plan"] .wpsubs-p1-node--plan,
+.wpsubs-p1-graph[data-focus="dur"] .wpsubs-p1-node--dur:not(.wpsubs-p1-node--ghost),
+.wpsubs-p1-graph[data-focus="prod"] .wpsubs-p1-node--prod:not(.wpsubs-p1-node--ghost) {
+ transform: scale(1.14);
+ z-index: 3;
+ border-color: var(--wpsubs-brand, #ff4d00);
+ box-shadow: 0 10px 22px rgba(255, 77, 0, 0.2);
+ animation: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .wpsubs-p1-flow,
+ .wpsubs-p1-graph .wpsubs-p1-node {
+ animation: none;
}
}
+
+/* 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-card__text {
+ min-width: 0;
+}
diff --git a/assets/js/admin/onboarding-wizard.js b/assets/js/admin/onboarding-wizard.js
index 583818dc..64a7980d 100644
--- a/assets/js/admin/onboarding-wizard.js
+++ b/assets/js/admin/onboarding-wizard.js
@@ -1,146 +1,323 @@
/**
- * 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.initFocusZoom();
+ this.updatePreview();
+ $("#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 -----
-
- goToPage1: function () {
- this.switchSection(1);
},
- goToPage2: function () {
- this.switchSection(2);
- },
+ // ----- Navigation -----
- goToPage3: function (productId) {
- $("#subscrpt-product-id").val(productId || 0);
- this.switchSection(3);
+ goToPage: function (pageNum, e) {
+ if (e && e.preventDefault) {
+ e.preventDefault();
+ }
+ this.switchSection(pageNum);
},
switchSection: function (pageNum) {
- this.currentPage = pageNum;
-
$("#subscrpt-wizard-page").val(pageNum);
- 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");
- }
+ $(".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");
- },
- // ----- Page 1 actions -----
+ // Swap the footer nav to this step's buttons.
+ $(".wpsubs-wizard-nav").attr("hidden", "hidden");
+ $('.wpsubs-wizard-nav[data-nav="' + pageNum + '"]').removeAttr("hidden");
+
+ // Light up the part of the preview this step fills in.
+ var groups = { 1: "plan", 2: "dur", 3: "prod", 4: "done" };
+ $("#subscrpt-preview-graph").attr("data-active", groups[pageNum] || "plan");
+ this.updatePreview();
+
+ $("html, body").animate({ scrollTop: 0 }, 150);
+ },
skip: function (e) {
e.preventDefault();
- window.location.href = this.subscriptionsUrl;
+ window.location.href = this.cfg.subscriptions_url;
},
- // ----- Page 2 actions -----
+ // ----- Page 1: plan type + name -----
+
+ selectPlanType: function (e) {
+ var card = $(e.currentTarget);
+ $(".wpsubs-plan-type-card").removeClass("active");
+ card.addClass("active");
+ $("#subscrpt-plan-type").val(card.data("type"));
+
+ // 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;
- 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();
},
- showExistingProduct: function (e) {
+ nextFromPlan: function (e) {
+ e.preventDefault();
+ if (!$.trim($("#subscrpt_plan_title").val())) {
+ window.alert("Please enter a plan name.");
+ return;
+ }
+ this.switchSection(2);
+ },
+
+ // ----- Page 2: live preview + create plan -----
+
+ initLivePreview: function () {
+ var self = this;
+ var update = function () {
+ self.updatePreview();
+ };
+ // All the fields whose typing should reflect into the preview graph.
+ $(document).on(
+ "input",
+ "#subscrpt_plan_title, #subscrpt_billing_frequency, #subscrpt_free_trial, #subscrpt_new_product_name",
+ update,
+ );
+ $(document).on("wpsubs:select", "#subscrpt-billing-interval-select, #subscrpt-trial-interval-select", update);
+ },
+
+ // Zoom the matching preview card while its field is focused.
+ initFocusZoom: function () {
+ var graph = $("#subscrpt-preview-graph");
+ var groups = [
+ { sel: "#subscrpt_plan_title", group: "plan" },
+ {
+ sel: "#subscrpt_billing_frequency, #subscrpt_free_trial, #subscrpt_signup_fee, #subscrpt-billing-interval-select, #subscrpt-trial-interval-select",
+ group: "dur",
+ },
+ { sel: "#subscrpt-product-search-input, #subscrpt_new_product_name, #subscrpt_connect_price", group: "prod" },
+ ];
+ groups.forEach(function (g) {
+ $(document).on("focusin", g.sel, function () {
+ graph.attr("data-focus", g.group);
+ });
+ $(document).on("focusout", g.sel, function () {
+ graph.attr("data-focus", "");
+ });
+ });
+ },
+
+ unitLabel: function (unit, count) {
+ var n = parseInt(count, 10) || 1;
+ return n > 1 ? n + " " + unit + "s" : unit;
+ },
+
+ // The name shown on the preview's product node, from whichever connect mode
+ // is active.
+ previewProductName: function () {
+ if (this.hasProducts && this.currentConnectMode() === "existing") {
+ return this.selectedProductName || "";
+ }
+ return $.trim($("#subscrpt_new_product_name").val());
+ },
+
+ // Fill the persistent preview graph from the current form state. Each step
+ // updates its own node; empty fields keep the placeholder label.
+ updatePreview: function () {
+ $("#subscrpt-preview-plan").text($.trim($("#subscrpt_plan_title").val()) || "Your plan");
+ $("#subscrpt-preview-plan-type").text($(".wpsubs-plan-type-card.active").data("label") || "Recurring");
+
+ var freq = $("#subscrpt_billing_frequency").val() || "1";
+ var interval = $("input[name='subscrpt_billing_interval']").val() || "month";
+ $("#subscrpt-preview-dur").text("Every " + this.unitLabel(interval, freq));
+
+ var prod = this.previewProductName();
+ if (prod) {
+ $("#subscrpt-preview-prod").text(prod);
+ }
+ },
+
+ 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") + " ›");
+ this.updatePreview();
},
openProductSearch: function () {
@@ -154,7 +331,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 +343,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 +362,137 @@
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;
+ this.updatePreview();
},
- 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();
- },
-
- 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();
- }
+ this.selectedProductName = "";
},
- // ----- 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) {
+ // The finished flow is shown by the persistent preview graph.
+ if (productName) {
+ $("#subscrpt-preview-prod").text(productName);
}
-
- return isValid;
+ this.switchSection(4);
},
- // ----- Page 3 actions -----
-
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, #subscrpt_new_product_name").val("");
+ $("#subscrpt_billing_frequency").val("1");
+ self.clearProduct();
+ // Reset the preview product node back to its placeholder.
+ $("#subscrpt-preview-prod").text("Product");
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/Links.php b/includes/Admin/Links.php
index a0445293..9527492d 100644
--- a/includes/Admin/Links.php
+++ b/includes/Admin/Links.php
@@ -24,9 +24,8 @@ public function __construct() {
* @param array $links Plugin Links.
*/
public function plugin_action_links( $links ) {
- // Getting Started link hidden for now — re-enable later.
- // $getting_started_url = admin_url( 'admin.php?page=wp-subscription-onboarding' );
- // array_unshift( $links, '' . __( 'Getting Started', 'subscription' ) . '' );
+ $getting_started_url = admin_url( 'admin.php?page=wp-subscription-onboarding' );
+ array_unshift( $links, '' . __( 'Getting Started', 'subscription' ) . '' );
if ( ! subscrpt_pro_activated() ) {
$links[] = '' . __( 'Upgrade to premium', 'subscription' ) . '';
}
diff --git a/includes/Admin/Menu.php b/includes/Admin/Menu.php
index 4b9ad6fa..916752db 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,
)
);
@@ -826,15 +840,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..dc332c3b 100644
--- a/includes/Admin/views/onboarding-wizard.php
+++ b/includes/Admin/views/onboarding-wizard.php
@@ -1,7 +1,13 @@
Set Frequency (duration) ->
+ * Connect to a Product -> Finish. A persistent right-rail preview shows the
+ * plan → durations → products flow and fills in live as each step is filled.
+ * 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,561 +16,500 @@
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',
+ ),
+);
+
+// Small SVG icons reused inside the preview graph nodes.
+$subscrpt_icon_cal = '';
+$subscrpt_icon_box = '';
+$subscrpt_icon_plan = '';
?>
-
-
+
+
+
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-