diff --git a/assets/css/admin.css b/assets/css/admin.css
index d0c7981551..58938bef6c 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -80,24 +80,6 @@ body.toplevel_page_progress-planner {
margin-top: var(--prpl-padding);
}
-/*------------------------------------*\
- Styles for the container of the page when the privacy policy is not accepted.
-\*------------------------------------*/
-.prpl-pp-not-accepted {
-
- .prpl-start-onboarding-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: var(--prpl-padding);
- }
-
- .prpl-start-onboarding-graphic {
- width: 250px;
- }
-}
-
/*------------------------------------*\
Generic styles.
\*------------------------------------*/
diff --git a/assets/css/onboard.css b/assets/css/onboard.css
new file mode 100644
index 0000000000..bac24ea7dd
--- /dev/null
+++ b/assets/css/onboard.css
@@ -0,0 +1,23 @@
+#prpl-onboarding-form .prpl-form-fields label {
+ display: grid;
+ grid-template-columns: 1fr 3fr;
+ margin-bottom: 0.5em;
+ gap: var(--prpl-padding);
+}
+
+#prpl-onboarding-form label > span:has(input[type="checkbox"]) {
+ display: flex;
+ align-items: baseline;
+}
+
+.prpl-onboard-form-radio-select {
+
+ label {
+ display: block !important;
+ }
+}
+
+#prpl-onboarding-submit-wrapper {
+ display: flex;
+ align-items: center;
+}
diff --git a/assets/css/onboarding/onboarding.css b/assets/css/onboarding/onboarding.css
deleted file mode 100644
index ae11f3057c..0000000000
--- a/assets/css/onboarding/onboarding.css
+++ /dev/null
@@ -1,1359 +0,0 @@
-.prpl-popover-onboarding {
-
- --prpl-color-text: #4b5563;
-
- /* Paper */
- --prpl-onboarding-popover-background: var(--prpl-background-paper, #fff);
-
- /* Steps navigation */
- --prpl-background-steps: var(--prpl-background, #f6f7f9);
- --prpl-background-info: var(--prpl-background-content, #f6f5fb);
- --prpl-background-step-active: var(--prpl-graph-color-4, #534786);
- --prpl-color-number-step-active: var(--prpl-background-paper, #fff);
- --prpl-color-ui-icon: #6b7280; /* already exists in variables-color.css */
- --prpl-color-text-step-active: var(--prpl-color-headings, #38296d);
- --prpl-color-border: #d1d5db; /* already exists in variables-color.css */
-
- /* Button secondary */
- --prpl-color-button-secondary: var(--prpl-background-point, #f9b23c );
- --prpl-color-button-secondary-hover: var(--prpl-color-monthly, #faa310);
- --prpl-color-button-secondary-text: #374151;
- --prpl-color-button-secondary-icon: var(--prpl-color-button-secondary-text);
-
- /* Button disabled */
- --prpl-color-button-inactive: var(--prpl-color-gauge-remain, #e1e3e7 );
- --prpl-color-button-inactive-text: var(--prpl-color-text, #4b5563);
-
- --prpl-background-step-label: var(--prpl-background-step-active);
- --prpl-color-step-label: var(--prpl-background-paper, #fff);
-
- /* Required text */
- --prpl-color-text-error: var(--prpl-color-button-primary, #dd324f);
- --prpl-color-alert-error: #e73136; /* already exists in variables-color.css */
-
- /* General error */
- --prpl-background-alert-error: #fdeded;/* already exists in variables-color.css */
- --prpl-color-alert-error-text: #7f1d1d; /* already exists in variables-color.css */
-
- /* Custom contros (inputs, radio, checkbox) */
- --prpl-color-selection-controls-inactive: #9ca3af;
- --prpl-color-selection-controls: var(--prpl-graph-color-4, #534786);
-
- --prpl-color-field-border: var(--prpl-color-border);
- --prpl-color-field-border-active: var(--prpl-color-alert-info, #2563eb);
- --prpl-background-field-active: var(--prpl-background-alert-info, #eff6ff);
-
-
- font-family: system-ui, Arial, sans-serif;
- font-size: 16px;
-
- padding: 0;
- box-sizing: border-box;
-
- border-radius: 8px;
- font-weight: 400;
- max-height: 82vh;
- width: 1200px;
- max-width: 80vw;
- color: var(--prpl-color-text);
- background-color: var(--prpl-onboarding-popover-background);
- border: 1px solid var(--prpl-color-border);
- box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.07),-2px 0 6px rgba(0, 0, 0, 0.07);
-
- /* Popover backdrop. */
- &::backdrop {
- background: rgba(0, 0, 0, 0.5);
- }
-
- /* Popover close button. */
- .prpl-popover-close {
- position: absolute;
- top: 5px;
- right: 5px;
- padding: 8px;
- cursor: pointer;
- background: none;
- border: none;
- color: var(--prpl-color-text);
- z-index: 10;
- }
-
- /* General styles. */
- & * {
- box-sizing: border-box;
- }
-
- p {
- font-size: 16px;
- margin-top: 0;
- margin-bottom: 8px;
-
- &:last-child {
- margin-bottom: 0;
- }
- }
-
- h2 {
- font-family: system-ui, Arial, sans-serif;
- margin-top: 0;
- margin-bottom: 12px;
- font-size: 20px;
- }
-
- h3 {
- font-family: system-ui, Arial, sans-serif;
- margin-top: 0;
- margin-bottom: 8px;
- font-size: 16px;
- font-weight: 600;
- color: var(--prpl-color-text);
-
- &:last-child {
- margin-bottom: 0;
- }
- }
-
- /* Copied from WP Core CSS. */
- select {
- font-size: 14px;
- line-height: 2;
- color: #2c3338;
- border-color: var(--prpl-color-field-border);
- box-shadow: none;
- border-radius: 3px;
- padding: 0 24px 0 8px;
- min-height: 30px;
-
- /* max-width: 400px; */
- width: 100%;
- -webkit-appearance: none;
- background: #fff url(data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E) no-repeat right 5px top 55%;
- background-size: 16px 16px;
- cursor: pointer;
- vertical-align: middle;
- }
-
- input[type="text"],
- input[type="email"] {
- width: 100%;
- padding: 8px;
- border: 1px solid var(--prpl-color-field-border);
- border-radius: 4px;
- font-size: 14px;
- line-height: 1.4;
- color: var(--prpl-color-text);
- }
-
- /* Used for radio and checkbox inputs. */
- .prpl-custom-inputs-wrapper{
- padding-left: 3px;
-
- /* To prevent custom radio and checkbox from being cut off. */
- display: flex;
- flex-direction: column;
- gap: 8px;
-
- .prpl-checkbox-wrapper,
- .prpl-radio-wrapper {
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- }
-
- /* Hide the default input, because WP has it's own styles (which include pseudo-elements). */
- .prpl-custom-checkbox input[type="checkbox"],
- .prpl-custom-radio input[type="radio"] {
- position: absolute;
- opacity: 0;
- width: 0;
- height: 0;
- }
-
- /* Shared styles for the custom radios and checkboxes. */
- .prpl-custom-control {
- display: inline-block;
- vertical-align: middle;
- margin-right: 12px;
- width: 20px;
- height: 20px;
- box-sizing: border-box;
- position: relative;
- transition: border-color 0.2s, background 0.2s;
- flex-shrink: 0;
- }
-
- /* Label text styling */
- .prpl-custom-checkbox,
- .prpl-custom-radio {
- display: flex;
- align-items: center;
- margin-bottom: 8px;
- cursor: pointer;
- user-select: none;
- }
-
- /* Checkbox styles */
- .prpl-custom-checkbox {
-
- .prpl-custom-control {
- border: 1px solid var(--prpl-color-selection-controls-inactive);
-
- /* border-radius: 6px; */
- background-color: var(--prpl-onboarding-popover-background);
- }
-
- input[type="checkbox"] {
-
- /* Checkbox checked (on) */
- &:checked+.prpl-custom-control {
- background: var(--prpl-color-selection-controls);
- border-color: var(--prpl-color-selection-controls);
- }
- }
-
- /* Checkmark */
- .prpl-custom-control::after {
- content: "";
- position: absolute;
- left: 6px;
- top: 2px;
- width: 4px;
- height: 9px;
- border: solid var(--prpl-onboarding-popover-background);
- border-width: 0 2px 2px 0;
- opacity: 0;
- transform: scale(0.8) rotate(45deg);
- transition: opacity 0.2s, transform 0.2s;
- }
-
- input[type="checkbox"]:checked+.prpl-custom-control::after {
- opacity: 1;
- transform: scale(1) rotate(45deg);
- }
- }
-
- /* Radio styles */
- .prpl-custom-radio {
-
- .prpl-custom-control {
- border: 1px solid var(--prpl-color-selection-controls-inactive);
- border-radius: 50%;
- background-color: var(--prpl-onboarding-popover-background);
- }
-
- /* Radio hover (off) */
- input[type="radio"] {
-
- /* Radio checked (on) */
- &:checked+.prpl-custom-control {
- background: var(--prpl-color-selection-controls);
- border-color: var(--prpl-color-selection-controls);
- }
- }
-
- /* Radio dot */
- .prpl-custom-control::after {
- content: "";
- position: absolute;
- top: 5px;
- left: 5px;
- width: 8px;
- height: 8px;
- background-color: var(--prpl-onboarding-popover-background);
- border-radius: 50%;
- opacity: 0;
- transition: opacity 0.2s;
- }
-
- input[type="radio"]:checked+.prpl-custom-control::after {
- opacity: 1;
- background-color: var(--prpl-onboarding-popover-background);
- }
- }
- }
-
- /* Main layout container */
- .prpl-onboarding-layout {
- display: flex;
-
- /* gap: 24px; */
- min-height: 350px;
- }
-
- /* Left column: Step navigation */
- .prpl-onboarding-navigation {
- width: 340px;
- background: var(--prpl-background-steps);
- border-right: none;
- padding: 32px 24px 24px 24px;
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- gap: 24px;
- }
-
- .prpl-onboarding-logo {
- height: 60px;
- max-height: 100%;
-
- img, svg {
- height: 100%;
- width: auto;
- max-width: 100%;
- }
- }
-
- .prpl-step-list {
- list-style: none;
- margin: 0;
- padding: 0;
- }
-
- .prpl-nav-step-item {
- margin-top: 10px;
- display: flex;
- align-items: flex-start;
- gap: 14px;
- padding: 0;
- margin-bottom: 4px;
- cursor: default;
-
- &:first-child {
- margin-top: 0;
- }
- }
-
- .prpl-step-icon {
- width: 20px;
- height: 20px;
- border-radius: 50%;
- border: 1px solid var(--prpl-color-border);
- color: var(--prpl-color-text);
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- margin-top: 2px;
- font-size: 14px;
- }
-
- .prpl-nav-step-item.prpl-active .prpl-step-icon {
- background: var(--prpl-background-step-active);
- border-color: var(--prpl-background-step-active);
- color: var(--prpl-color-number-step-active);
- }
-
- .prpl-nav-step-item.prpl-completed .prpl-step-icon {
- background: var(--prpl-color-ui-icon);
- border-color: var(--prpl-color-ui-icon);
- color: var(--prpl-background-paper);
- }
-
- .prpl-step-label {
- font-size: 15px;
- color: var(--prpl-color-ui-icon);
- line-height: 1.5;
- }
-
- .prpl-nav-step-item.prpl-active .prpl-step-label {
- color: var(--prpl-color-text-step-active);
- font-weight: 700;
- }
-
- .prpl-nav-step-item.prpl-completed .prpl-step-label {
- color: var(--prpl-color-text-step-active);
- }
-
- #prpl-onboarding-mobile-step-label {
- display: none;
- margin-bottom: 4px;
- font-size: 14px;
- color: var(--prpl-background-step-active);
- font-weight: 600;
- }
-
- /* Right section: Content area */
- .prpl-onboarding-content {
- padding: 32px 24px;
- flex: 1;
- display: flex;
- flex-direction: column;
-
- /* gap: 24px; */
- }
-
- .tour-content-wrapper {
- flex: 1;
-
- /* overflow-y: auto; */
- }
-
- .tour-header {
-
- .tour-title {
- color: var(--prpl-background-step-active);
- font-weight: 600;
- }
- }
-
- .tour-footer {
- margin-top: 16px;
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 10px;
- width: 100%;
-
- .prpl-tour-next-wrapper {
- margin-left: auto;
- display: flex;
- align-items: center;
- gap: 10px;
- }
- }
-
- .tour-content {
- font-size: 16px;
- line-height: 1.5;
-
- img {
- max-width: 100%;
- height: auto;
- }
-
- }
-
- .prpl-columns-wrapper-flex {
- display: flex;
- gap: 24px;
-
- /* overflow: hidden; */
-
- /* padding-bottom: 10px; */
-
- .prpl-background-content {
- padding: 20px;
- border-radius: 12px;
- background-color: var(--prpl-background-info);
- }
-
- .prpl-column {
- flex-grow: 1;
- flex-basis: 50%;
- flex-direction: column;
- }
-
- &.prpl-columns-2-1 {
-
- .prpl-column:first-child {
- flex: 2 1 0;
- min-width: 0;
- }
-
- .prpl-column:last-child {
- flex: 1 1 0;
- min-width: 0;
- }
- }
-
- &.prpl-columns-1-2 {
-
- .prpl-column:first-child {
- flex: 1 1 0;
- min-width: 0;
- }
-
- .prpl-column:last-child {
- flex: 2 1 0;
- min-width: 0;
- }
- }
- }
-
- .prpl-btn {
- display: inline-block;
- margin: 0;
- padding: 12px 20px;
-
- text-decoration: none;
- cursor: pointer;
- font-size: 16px;
-
- /* color: var(--prpl-color-button-primary-text); */
-
- /* background: var(--prpl-color-button-primary); */
- background-color: var(--prpl-color-button-secondary);
- color: var(--prpl-color-button-secondary-text);
- line-height: 1.25;
- box-shadow: none;
- border: none;
- border-radius: 6px;
- transition: all 0.25s ease-in-out;
- font-weight: 600;
- text-align: center;
- box-sizing: border-box;
- position: relative;
- z-index: 1;
-
- flex-shrink: 0;
-
- /* &:not([disabled]):hover,
- &:not([disabled]):focus {
- background: var(--prpl-color-button-primary-hover);
- } */
-
- &.prpl-btn-secondary {
- background-color: var(--prpl-color-button-secondary);
- color: var(--prpl-color-button-secondary-text);
-
- &:not([disabled]):not(.prpl-btn-disabled):hover,
- &:not([disabled]):not(.prpl-btn-disabled):focus {
- background-color: var(--prpl-color-button-secondary-hover);
- color: var(--prpl-color-button-secondary-text);
- }
- }
-
- &.prpl-btn-disabled,
- &:disabled {
-
- &,
- &:hover{
- color: var(--prpl-color-button-inactive-text);
- color: rgb(from var(--prpl-color-button-inactive-text) r g b / 0.88);
- background-color: var(--prpl-color-button-inactive);
- cursor: not-allowed;
- }
- }
- }
-
- .prpl-complete-task-btn:not(.prpl-btn) {
- border: none;
- background: none;
- cursor: pointer;
- padding: 0;
- margin: 0;
- font-size: 16px;
- color: var(--prpl-color-link);
- }
-
-/*
- .prpl-complete-task-btn-completed:not(.prpl-btn) {
- color: #059669;
- pointer-events: none;
- opacity: 0.5;
- } */
-
- .prpl-complete-task-btn-error {
- color: #9f0712;
- }
-
- /* Generic error message (reusable for all steps) */
- .prpl-error-message {
- animation: prpl-slide-down 0.3s ease-out;
- }
-
- @keyframes prpl-slide-down {
-
- from {
- opacity: 0;
- transform: translateY(-10px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-
- .prpl-error-box {
- display: flex;
- align-items: flex-start;
- gap: 15px;
- background: var(--prpl-background-alert-error);
- padding: 20px;
- border-radius: 6px;
-
- .prpl-error-icon {
- color: var(--prpl-color-alert-error);
- width: 20px;
- height: 20px;
- flex-shrink: 0;
-
- img, svg {
- width: 100%;
- height: 100%;
- }
- }
-
- h3 {
- margin: 0 0 8px 0;
- font-size: 16px;
- font-weight: 700;
- color: var(--prpl-color-alert-error-text);
- }
-
- p {
- margin: 0;
- font-size: 16px;
- line-height: 1.4;
- color: var(--prpl-color-alert-error-text);
- }
- }
-
- /* Popover: Quit confirmation */
- &:has(.prpl-quit-confirmation) {
-
- #prpl-tour-close-btn,
- .prpl-onboarding-navigation {
- display: none;
- }
- }
-
- .prpl-column:has(.prpl-quit-confirmation) {
- justify-content: flex-start !important;
- }
-
- .prpl-quit-confirmation {
- display: flex;
- flex-direction: column;
- gap: 30px;
- }
-
- .prpl-quit-message {
- display: flex;
- align-items: flex-start;
- gap: 15px;
- background: var(--prpl-background-alert-error);
- padding: 20px;
- border-radius: 6px;
- border-left: 4px solid var(--prpl-color-alert-error);
-
- h3 {
- font-size: 18px;
- font-weight: 700;
- }
-
- p {
- font-size: 15px;
- line-height: 1.6;
- }
- }
-
- .prpl-quit-actions {
- display: flex;
- gap: 40px;
- justify-content: space-between;
- align-items: center;
- }
-
- .prpl-quit-link {
- font-size: 15px;
- color: var(--prpl-color-link);
- text-decoration: underline;
- cursor: pointer;
-
- &.prpl-quit-link-primary {
- font-weight: 600;
- font-size: 16px;
- }
- }
-
- #prpl-quit-confirmation-graphic {
- display: flex;
- justify-content: center;
- align-items: center;
-
- img,
- svg {
- height: 250px;
-
- /* Set height since it is what we do in the PP dashboard. */
- max-width: 100%;
- }
- }
-
- /* Form for onboarding tasks. */
- .prpl-onboarding-task-form {
- display: flex;
- align-items: center;
- gap: 8px;
- border: none;
- padding: 0;
- margin: 0;
- background: none;
-
- .prpl-complete-task-btn {
- flex-shrink: 0;
- margin-top: 16px;
- }
- }
-
- /* Welcome Step */
- &[data-prpl-step="0"] {
-
- .prpl-column:not(.prpl-column-content) {
- display: flex;
- justify-content: center;
- align-items: center;
- }
-
- #prpl-welcome-graphic {
- display: flex;
- justify-content: center;
- align-items: center;
-
- img, svg {
- height: 250px; /* Set height since it is what we do in the PP dashboard. */
- max-width: 100%;
- }
- }
-
- .prpl-welcome-note{
- font-size: 14px;
- font-style: italic;
- }
- }
-
- /* Privacy checkbox */
- .prpl-privacy-checkbox-wrapper {
- margin-top: 20px;
-
- label {
- gap: 10px;
- cursor: pointer;
- font-size: 15px;
- color: var(--prpl-color-text);
- }
-
- input[type="checkbox"] {
- width: 18px;
- height: 18px;
- cursor: pointer;
- }
-
- a {
- color: var(--prpl-color-link);
- text-decoration: underline;
- }
-
- .prpl-required-indicator {
- font-size: 14px;
- font-style: italic;
-
- &::before {
- content: '(';
- }
-
- &::after {
- content: ')';
- }
-
- &.prpl-required-indicator-active {
- color: var(--prpl-color-text-error);
- font-size: 16px;
- font-style: normal;
-
- &::before {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- content: '!';
- width: 16px;
- height: 16px;
- border-radius: 50%;
- background-color: var(--prpl-color-alert-error);
- color: #fff;
- font-size: 12px;
- margin-right: 5px;
- }
-
- &::after {
- content: '!';
- }
- }
- }
- }
-
- /* What's Next Step */
- .prpl-suggested-task-points {
- font-size: var(--prpl-font-size-xs, 12px);
- font-weight: 700;
- color: var(--prpl-text-point);
- background-color: var(--prpl-background-point);
- width: 24px;
- height: 24px;
- border-radius: 50%;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- }
-
- /* First Task Step */
- .prpl-onboarding-task {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- gap: 10px;
-
- /* .prpl-onboarding-task-title {
- margin: 0 0 16px 0;
- font-size: 18px;
- font-weight: 600;
- } */
-
- .prpl-onboarding-task-form {
- margin-top: 16px;
- width: 100%;
- flex-direction: column;
-
- .prpl-complete-task-btn {
- align-self: flex-end;
-
- &.prpl-complete-task-btn-completed {
- opacity: 0.5;
- pointer-events: none;
- }
- }
- }
- }
-
- /* Badges Step */
- .prpl-gauge-wrapper {
- max-width: 100%;
- text-align: center;
- }
-
- /* Email Frequency Step */
-
- /* .prpl-email-frequency-options {
-
- .
- display: flex;
- gap: 10px;
- flex-direction: column;
- } */
-
- #prpl-email-form {
- margin-top: 16px;
-
- .prpl-form-field {
- margin-top: 16px;
-
- &:first-child {
- margin-top: 0;
- }
-
- label {
- display: inline-block;
- margin-bottom: 8px;
- }
- }
- }
-
- /* Settings Step */
- .tour-content-wrapper:has(.prpl-setting-item) {
- display: flex;
- flex-direction: column;
- flex-grow: 1;
-
- .tour-content,
- .prpl-setting-item,
- .prpl-setting-content {
- display: flex;
- flex-direction: column;
- flex-grow: 1;
- gap: 24px;
- }
-
- .prpl-setting-title {
- margin: 0 0 16px 0;
- }
-
- .prpl-settings-progress {
- font-size: 14px;
- color: var(--prpl-color-step-label);
- padding: 4px 8px;
- border-radius: 20px;
- background-color: var(--prpl-background-step-active);
- }
-
- .prpl-setting-footer {
- display: flex;
- justify-content: flex-end;
- align-items: flex-start;
- gap: 10px;
- margin-top: 16px;
-
- .prpl-save-setting-btn {
- flex-shrink: 0;
- }
- }
-
- .prpl-select-page {
-
- &.prpl-disabled {
- opacity: 0.5;
- pointer-events: none;
- }
- }
-
- .prpl-setting-note {
- display: none;
- gap: 10px;
- border-radius: 12px;
- padding: 16px;
- color: var(--prpl-color-field-border-active);
- font-size: 14px;
- background-color: var(--prpl-background-field-active);
-
- .prpl-setting-note-icon {
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px;
- border-radius: 50%;
- color: var(--prpl-color-field-border-active);
- }
- }
-
- /* Post types sub-step */
- #prpl-post-types-include-wrapper {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
-
- /* Toggle checkbox for post types. */
- .prpl-post-type-toggle-wrapper {
- display: flex;
- align-items: center;
- }
-
- .prpl-post-type-toggle-label {
- display: flex;
- align-items: center;
- gap: 12px;
- cursor: pointer;
- position: relative;
- }
-
- .prpl-post-type-toggle-input {
- position: absolute;
- opacity: 0;
- width: 0;
- height: 0;
- margin: 0;
- padding: 0;
- }
-
- .prpl-post-type-toggle-switch {
- position: relative;
- width: 44px;
- height: 24px;
- border-radius: 12px;
- background-color: var(--prpl-color-selection-controls-inactive);
- transition: background-color 0.2s;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
-
- &::after {
- content: '';
- position: absolute;
- top: 2px;
- left: 2px;
- width: 20px;
- height: 20px;
- border-radius: 50%;
- background-color: var(--prpl-onboarding-popover-background);
- transition: transform 0.2s;
- z-index: 1;
- }
-
- svg {
- position: absolute;
- width: 12px;
- height: 12px;
- top: 50%;
- left: 6px;
- transform: translateY(-50%);
- z-index: 2;
- transition: opacity 0.2s, left 0.2s, color 0.2s;
- color: var(--prpl-color-ui-icon);
- }
-
- .prpl-toggle-icon-check {
- display: none;
- }
-
- .prpl-toggle-icon-x {
- display: block;
- }
- }
-
- .prpl-post-type-toggle-label:hover .prpl-post-type-toggle-switch svg {
- opacity: 0.6;
- }
-
- .prpl-post-type-toggle-input:checked ~ .prpl-post-type-toggle-switch {
- background-color: var(--prpl-background-step-active);
-
- &::after {
- transform: translateX(20px);
- }
-
- svg {
- left: 26px;
- color: var(--prpl-background-step-active);
- transform: translateY(-50%);
- }
-
- .prpl-toggle-icon-check {
- display: block;
- }
-
- .prpl-toggle-icon-x {
- display: none;
- }
- }
-
- .prpl-post-type-toggle-text {
- font-size: 16px;
- line-height: 1.5;
- transition: opacity 0.2s;
- }
-
- .prpl-post-type-toggle-input:not(:checked) ~ .prpl-post-type-toggle-text {
- opacity: 0.78;
- }
-
- .prpl-post-type-toggle-input:checked ~ .prpl-post-type-toggle-text {
- opacity: 1;
- }
- }
-
- /* More Tasks Step */
-
- #prpl-success-graphic {
- display: flex;
- justify-content: center;
- align-items: center;
-
- img, svg {
- height: 250px;
- max-width: 100%;
- }
- }
-
- /* Intro substep */
- .prpl-more-tasks-substep[data-substep="more-tasks-intro"] {
- display: flex;
- flex-direction: column;
- gap: 24px;
- }
-
- .prpl-more-tasks-intro-buttons {
- margin-top: 16px;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 24px;
- flex-wrap: wrap;
- }
-
- .prpl-finish-onboarding {
- background: none;
- border: none;
- color: var(--prpl-color-link, #534786);
- text-decoration: underline;
- font-size: 16px;
- cursor: pointer;
- padding: 12px 0;
- }
-
- .prpl-finish-onboarding:hover,
- .prpl-finish-onboarding:focus {
- color: var(--prpl-color-link-hover, #38296d);
- }
-
- /* Tasks substep */
- .prpl-more-tasks-substep[data-substep="more-tasks-tasks"] .prpl-task-list {
- margin: 0;
- }
-
- .prpl-column:has(.prpl-task-list) {
- display: flex;
- align-items: center;
- }
-
- .prpl-task-list {
- list-style: none;
- padding: 0;
- margin: 0;
- width: 100%;
- border: 1px solid #ccc;
-
- li {
- padding: 7px 5px;
- margin: 0;
-
- &:nth-child(odd) {
- background-color: var(--prpl-background-steps);
- }
-
- .task-title {
- color: var(--prpl-color-text);
- font-weight: 500;
- }
- }
- }
-
- .prpl-complete-task-item {
- display: flex;
- gap: 30px;
- justify-content: space-between;
- }
-
- .prpl-task-arrow {
- padding-right: 8px;
- }
-
- .prpl-task-item-button-wrapper {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-shrink: 0;
- }
-
- .prpl-task-completed-icon {
- display: none;
- width: 20px;
- height: 20px;
- border-radius: 50%;
- align-items: center;
- justify-content: center;
- color: #fff;
- background-color: #059669;
- }
-
- .prpl-task-completed {
-
- /* Show the completed icon. */
- .prpl-task-completed-icon {
- display: inline-flex;
- }
-
- /* Hide the trigger button and +1. */
- .prpl-task-item-button-wrapper {
- display: none;
- }
- }
-
- .tour-content-wrapper:has(.prpl-task-content-active) {
- display: flex;
- flex-direction: column;
- flex-grow: 1;
-
- /* Task content active, TOOD: change markup so this is simpler. */
- .prpl-task-content-active {
- display: flex;
- flex-direction: column;
- flex-grow: 1;
- gap: 24px;
- width: 100%;
-
- .prpl-onboarding-task {
- display: flex;
- flex-direction: column;
- flex-grow: 1;
-
- .prpl-onboarding-task-form,
- .tour-content {
- display: flex;
- flex-direction: column;
- flex-grow: 1;
- }
-
- .prpl-onboarding-task-form {
- justify-content: space-between;
- gap: 48px;
- }
- }
- }
-
- /* Task buttons. */
- .prpl-task-buttons {
- display: flex;
- justify-content: flex-end;
- gap: 16px;
- width: 100%;
-
- .prpl-btn {
- margin-top: 0;
- margin-bottom: 0;
-
- &.prpl-task-close-btn {
- background-color: var(--prpl-background-banner);
- color: var(--prpl-color-text);
- }
- }
- }
- }
-
- /* File drop zone. */
- .prpl-file-drop-zone {
- width: 100%;
- border-radius: 12px;
- padding: 40px;
- text-align: center;
- color: var(--prpl-color-text);
- transition: background 0.2s, border-color 0.2s;
- cursor: pointer;
- border: 2px dashed var(--prpl-color-field-border);
-
- svg {
-
- path {
- stroke: var(--prpl-color-ui-icon);
- }
- }
- }
-
- .prpl-file-drop-zone.dragover {
- background-color: var(--prpl-background-field-active);
- border-color: var(--prpl-color-field-border-active);
- }
-
- /* When an image has been uploaded. */
- .prpl-file-drop-zone.has-image {
- border-color: var(--prpl-color-field-border-active);
- background-color: var(--prpl-background-field-active);
-
- & > .prpl-icon-image,
- & > p,
- & > .prpl-file-upload-hints {
- display: none;
- }
-
- .prpl-file-preview img {
- border: 2px solid var(--prpl-color-field-border);
- border-radius: 8px;
- padding: 8px;
- background: var(--prpl-color-background-white);
- }
- }
-
- .prpl-file-browse-link {
- color: var(--prpl-color-link);
- text-decoration: underline;
- cursor: pointer;
- }
-
- .prpl-file-remove-btn {
- border: none;
- background: none;
- cursor: pointer;
- padding: 0;
- margin: 12px 0 0;
- font-size: 16px;
- color: var(--prpl-color-link);
- text-decoration: underline;
- }
-
- .prpl-file-upload-hints {
- display: flex;
- flex-direction: column;
- font-size: 14px;
- color: var(--prpl-color-ui-icon);
- }
-
- /* WIP */
- #prpl-upload-status {
- margin-top: 10px;
- font-family: monospace;
- }
-
- .prpl-file-preview {
- display: none; /* Hidden by default. */
- margin-top: 10px;
- margin-left: auto;
- margin-right: auto;
- max-width: 200px;
- height: auto;
- }
-}
-
-@media (max-width: 1023px) {
-
- .prpl-popover-onboarding {
- --prpl-mobile-nav-height: 60px;
- max-width: 90vw;
- inset: 0 0 var(--prpl-mobile-nav-height) 0; /* TODO: Adjust this for smallest screen sizes. */
- margin: auto auto var(--prpl-mobile-nav-height) auto; /* Center in available space above nav */
-
- /* Hide graphics */
- .prpl-hide-on-mobile {
- display: none !important;
- }
-
- /* Quit confirmation */
- .prpl-quit-actions {
- flex-direction: column;
- gap: 16px;
- }
-
- /* Naviation section */
- .prpl-onboarding-navigation {
- position: fixed;
- left: 0;
- bottom: 0;
- width: 100%;
- height: var(--prpl-mobile-nav-height);
- z-index: 1000;
-
- flex-direction: row;
- gap: 16px;
- justify-content: center;
- align-items: center;
- padding: 8px;
-
- .prpl-step-list {
- display: flex;
- gap: 8px;
-
- .prpl-nav-step-item {
- margin: 0;
- }
-
- .prpl-step-label {
- display: none;
- }
- }
-
- #prpl-onboarding-mobile-step-label {
- display: block;
- }
- }
-
- /* Content section */
- .prpl-columns-wrapper-flex {
- flex-direction: column;
- }
-
- .prpl-setting-footer, /* On the settings steps */
- .tour-footer {
- flex-direction: column; /* So the info / error message is on top of the button. */
- }
-
- /* Badges step */
- .prpl-gauge-wrapper {
- max-width: 400px;
- margin-left: auto;
- margin-right: auto;
- }
- }
-}
diff --git a/assets/css/welcome.css b/assets/css/welcome.css
new file mode 100644
index 0000000000..20004600b4
--- /dev/null
+++ b/assets/css/welcome.css
@@ -0,0 +1,77 @@
+.prpl-wrap.prpl-pp-not-accepted {
+ padding: 0;
+ background-color: var(--prpl-background-paper);
+ border: 1px solid var(--prpl-color-border);
+ border-radius: var(--prpl-border-radius);
+}
+
+.prpl-welcome {
+
+ .inner-content {
+ padding: calc(var(--prpl-gap) * 1.5);
+ padding-bottom: 0;
+ margin-bottom: calc(var(--prpl-gap) * 1.5);
+ display: flex;
+ grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
+ gap: calc(var(--prpl-gap) * 2);
+
+ .left {
+ flex-grow: 1;
+ }
+
+ img {
+ max-width: 100%;
+ width: 550px;
+ height: auto;
+ }
+ }
+
+ .welcome-header {
+ background: var(--prpl-background-banner);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-top-left-radius: var(--prpl-border-radius);
+ border-top-right-radius: var(--prpl-border-radius);
+ overflow: hidden;
+
+ h1 {
+ font-size: var(--prpl-font-size-3xl);
+ padding: var(--prpl-padding) calc(var(--prpl-gap) * 1.5);
+ font-weight: 600;
+ }
+
+ .welcome-header-icon {
+ background: var(--prpl-background-banner);
+ background: linear-gradient(105deg, var(--prpl-background-banner) 25%, var(--prpl-background-monthly) 25%);
+ padding: var(--prpl-padding);
+ padding-left: 100px;
+ padding-right: calc(var(--prpl-gap) * 1.5);
+
+ svg {
+ height: 100px;
+ }
+ }
+ }
+
+ .prpl-form-notice-title {
+ font-size: var(--prpl-font-size-lg);
+ }
+
+ ul {
+ list-style: disc;
+ margin-left: 1rem;
+ }
+
+ .prpl-onboard-form-radio-select {
+ margin-top: 0.75rem;
+
+ label {
+ margin-top: 0.5rem;
+
+ &:first-child {
+ margin-top: 0;
+ }
+ }
+ }
+}
diff --git a/assets/images/onboarding/icon_image.svg b/assets/images/onboarding/icon_image.svg
deleted file mode 100644
index 24f79c262a..0000000000
--- a/assets/images/onboarding/icon_image.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/assets/images/onboarding/icon_info_solid.svg b/assets/images/onboarding/icon_info_solid.svg
deleted file mode 100644
index 9b6e2d98f2..0000000000
--- a/assets/images/onboarding/icon_info_solid.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/assets/images/onboarding/neglected_site_ravi.svg b/assets/images/onboarding/neglected_site_ravi.svg
deleted file mode 100644
index 14acee56f5..0000000000
--- a/assets/images/onboarding/neglected_site_ravi.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/assets/images/onboarding/success_ravi.svg b/assets/images/onboarding/success_ravi.svg
deleted file mode 100644
index 45e8535d4e..0000000000
--- a/assets/images/onboarding/success_ravi.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/images/onboarding/thumbs_up_ravi_rtl.svg b/assets/images/onboarding/thumbs_up_ravi_rtl.svg
deleted file mode 100644
index 27234bc510..0000000000
--- a/assets/images/onboarding/thumbs_up_ravi_rtl.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/assets/js/license-generator.js b/assets/js/license-generator.js
deleted file mode 100644
index 5dfc2bb109..0000000000
--- a/assets/js/license-generator.js
+++ /dev/null
@@ -1,140 +0,0 @@
-/**
- * License Generator - Handles license key generation during onboarding
- * Adapted from onboard.js
- */
-/* global progressPlanner */
-
-// eslint-disable-next-line no-unused-vars
-class LicenseGenerator {
- /**
- * Store config for use in other methods.
- *
- * @type {Object}
- */
- static config = null;
-
- /**
- * Get the default config from progressPlanner global.
- *
- * @return {Object} Default configuration object.
- */
- static getDefaultConfig() {
- // eslint-disable-next-line no-undef
- if ( typeof progressPlanner !== 'undefined' ) {
- return {
- // eslint-disable-next-line no-undef
- onboardNonceURL: progressPlanner.onboardNonceURL,
- // eslint-disable-next-line no-undef
- onboardAPIUrl: progressPlanner.onboardAPIUrl,
- // eslint-disable-next-line no-undef
- adminAjaxUrl: progressPlanner.ajaxUrl,
- // eslint-disable-next-line no-undef
- nonce: progressPlanner.nonce,
- };
- }
- return {};
- }
-
- /**
- * Make a request to save the license key.
- *
- * @param {string} licenseKey The license key.
- * @return {Promise} Promise that resolves when license is saved
- */
- static saveLicenseKey( licenseKey ) {
- console.log( 'License key: ' + licenseKey );
- return LicenseGenerator.ajaxRequest( {
- url: LicenseGenerator.config.adminAjaxUrl,
- data: {
- action: 'progress_planner_save_onboard_data',
- _ajax_nonce: LicenseGenerator.config.nonce,
- key: licenseKey,
- },
- } );
- }
-
- /**
- * Make the AJAX request to the API.
- *
- * @param {Object} data The data to send with the request.
- * @return {Promise} Promise that resolves when request completes
- */
- static ajaxAPIRequest( data ) {
- return LicenseGenerator.ajaxRequest( {
- url: LicenseGenerator.config.onboardAPIUrl,
- data,
- } )
- .then( ( response ) => {
- // Make a local request to save the response data.
- return LicenseGenerator.saveLicenseKey( response.license_key );
- } )
- .catch( ( error ) => {
- console.warn( error );
- throw error;
- } );
- }
-
- /**
- * Make the AJAX request.
- *
- * Make a request to get the nonce.
- * Once the nonce is received, make a request to the API.
- *
- * @param {Object} data The data to send with the request.
- * @param {Object} config Optional configuration object. Falls back to progressPlanner global.
- * @return {Promise} Promise that resolves when license is generated
- */
- static generateLicense( data = {}, config = null ) {
- // Store config for use in other methods, fall back to default if not provided.
- LicenseGenerator.config = config || LicenseGenerator.getDefaultConfig();
-
- return LicenseGenerator.ajaxRequest( {
- url: LicenseGenerator.config.onboardNonceURL,
- data,
- } ).then( ( response ) => {
- if ( 'ok' === response.status ) {
- // Add the nonce to our data object.
- data.nonce = response.nonce;
-
- // Make the request to the API.
- return LicenseGenerator.ajaxAPIRequest( data );
- }
- // Handle error response
- const errorMessage =
- response.message ||
- 'Failed to get nonce for license generation';
- throw new Error( errorMessage );
- } );
- }
-
- /**
- * Helper function to make AJAX requests
- *
- * @param {Object} options Request options
- * @param {string} options.url The URL to send the request to
- * @param {Object} options.data The data to send with the request
- * @return {Promise} Promise that resolves with response data
- */
- static ajaxRequest( options ) {
- const { url, data } = options;
-
- return fetch( url, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- body: new URLSearchParams( data ),
- credentials: 'same-origin',
- } )
- .then( ( response ) => {
- if ( ! response.ok ) {
- throw new Error( 'Request failed: ' + response.status );
- }
- return response.json();
- } )
- .catch( ( error ) => {
- console.error( 'AJAX request error:', error );
- throw error;
- } );
- }
-}
diff --git a/assets/js/onboard.js b/assets/js/onboard.js
new file mode 100644
index 0000000000..529b05831b
--- /dev/null
+++ b/assets/js/onboard.js
@@ -0,0 +1,168 @@
+/* global progressPlanner, progressPlannerAjaxRequest */
+/*
+ * Onboard
+ *
+ * A script to handle the onboarding process.
+ *
+ * Dependencies: progress-planner/ajax-request, progress-planner/upgrade-tasks
+ */
+
+/**
+ * Make a request to save the license key.
+ *
+ * @param {string} licenseKey The license key.
+ */
+const progressPlannerSaveLicenseKey = ( licenseKey ) => {
+ console.log( 'License key: ' + licenseKey );
+ return progressPlannerAjaxRequest( {
+ url: progressPlanner.ajaxUrl,
+ data: {
+ action: 'progress_planner_save_onboard_data',
+ _ajax_nonce: progressPlanner.nonce,
+ key: licenseKey,
+ },
+ } );
+};
+
+/**
+ * Make the AJAX request.
+ *
+ * @param {Object} data The data to send with the request.
+ */
+const progressPlannerAjaxAPIRequest = ( data ) => {
+ progressPlannerAjaxRequest( {
+ url: progressPlanner.onboardAPIUrl,
+ data,
+ } )
+ .then( ( response ) => {
+ // Make a local request to save the response data.
+ progressPlannerSaveLicenseKey( response.license_key ).then( () => {
+ // Refresh the page.
+ window.location.reload();
+ } );
+ } )
+ .catch( ( error ) => {
+ console.warn( error );
+ } );
+};
+
+/**
+ * Make the AJAX request.
+ *
+ * Make a request to get the nonce.
+ * Once the nonce is received, make a request to the API.
+ *
+ * @param {Object} data The data to send with the request.
+ */
+const progressPlannerOnboardCall = ( data ) => {
+ progressPlannerAjaxRequest( {
+ url: progressPlanner.onboardNonceURL,
+ data,
+ } ).then( ( response ) => {
+ if ( 'ok' === response.status ) {
+ // Add the nonce to our data object.
+ data.nonce = response.nonce;
+
+ // Make the request to the API.
+ progressPlannerAjaxAPIRequest( data );
+ }
+ } );
+};
+
+if ( document.getElementById( 'prpl-onboarding-form' ) ) {
+ document
+ .querySelectorAll( 'input[name="with-email"]' )
+ .forEach( ( input ) => {
+ input.addEventListener( 'change', function () {
+ if ( 'no' === this.value ) {
+ document
+ .getElementById( 'prpl-onboarding-form' )
+ .querySelectorAll( 'input' )
+ .forEach( ( inputField ) => {
+ inputField.required = false;
+ } );
+ } else {
+ document
+ .getElementById( 'prpl-onboarding-form' )
+ .querySelectorAll( 'input' )
+ .forEach( ( inputField ) => {
+ if (
+ 'name' === inputField.name ||
+ 'email' === inputField.name
+ ) {
+ inputField.required = true;
+ }
+ } );
+ }
+ document
+ .getElementById( 'prpl-onboarding-form' )
+ .querySelectorAll(
+ '.prpl-form-fields, .prpl-form-fields, .prpl-button-primary, .prpl-button-secondary--no-email'
+ )
+ .forEach( ( el ) => el.classList.toggle( 'prpl-hidden' ) );
+ } );
+ } );
+
+ document
+ .querySelector( '#prpl-onboarding-form input[name="privacy-policy"]' )
+ .addEventListener( 'change', function () {
+ const privacyPolicyAccepted = !! this.checked;
+
+ if ( privacyPolicyAccepted ) {
+ document
+ .getElementById( 'prpl-onboarding-submit-wrapper' )
+ .classList.remove( 'prpl-disabled' );
+ } else {
+ document
+ .getElementById( 'prpl-onboarding-submit-wrapper' )
+ .classList.add( 'prpl-disabled' );
+ }
+ } );
+
+ document
+ .getElementById( 'prpl-onboarding-form' )
+ .addEventListener( 'submit', function ( event ) {
+ event.preventDefault();
+
+ const privacyPolicyAccepted = !! document.querySelector(
+ '#prpl-onboarding-form input[name="privacy-policy"]'
+ ).checked;
+
+ // Make sure the user accepted the privacy policy.
+ if ( ! privacyPolicyAccepted ) {
+ return;
+ }
+
+ // Disable all (both buttons) submit buttons.
+ document
+ .querySelectorAll(
+ '#prpl-onboarding-form input[type="submit"]'
+ )
+ .forEach( ( input ) => {
+ input.disabled = true;
+ } );
+
+ // Show the spinner.
+ const spinner = document.createElement( 'span' );
+ spinner.classList.add( 'prpl-spinner' );
+ spinner.innerHTML =
+ ''; // WP spinner.
+
+ // Append spinner after submit button.
+
+ document
+ .getElementById( 'prpl-onboarding-submit-wrapper' )
+ .appendChild( spinner );
+
+ // Get all form data.
+ const data = Object.fromEntries( new FormData( event.target ) );
+
+ // If the user doesn't want to use email, remove the email and name.
+ if ( 'no' === data.with_email ) {
+ data.email = '';
+ data.name = '';
+ }
+
+ progressPlannerOnboardCall( data );
+ } );
+}
diff --git a/assets/js/onboarding/OnboardTask.js b/assets/js/onboarding/OnboardTask.js
deleted file mode 100644
index 2db23b7816..0000000000
--- a/assets/js/onboarding/OnboardTask.js
+++ /dev/null
@@ -1,470 +0,0 @@
-/**
- * OnboardTask - Handles individual tasks that open within the column
- * Used by the MoreTasksStep for tasks that require user input
- * Toggles visibility of task list and shows task content in the same column
- */
-/* global ProgressPlannerOnboardData, ProgressPlannerTourUtils */
-
-// eslint-disable-next-line no-unused-vars
-class PrplOnboardTask {
- constructor( el, wizard ) {
- this.el = el;
- this.id = el.dataset.taskId;
- this.wizard = wizard;
- this.taskContent = null;
- this.formValues = {};
- this.openTaskBtn = el.querySelector( '[prpl-open-task]' );
-
- // Register task open event
- this.openTaskBtn?.addEventListener( 'click', () => this.open() );
- }
-
- /**
- * Get the tour footer element via the current step
- * @return {HTMLElement|null} The tour footer element or null if not found
- */
- getTourFooter() {
- // Get the current step and use its getTourFooter method
- const currentStep =
- this.wizard?.tourSteps?.[ this.wizard.state.currentStep ];
- if ( currentStep && typeof currentStep.getTourFooter === 'function' ) {
- return currentStep.getTourFooter();
- }
-
- // Fallback in case step doesn't have the method
- return this.wizard?.contentWrapper?.querySelector( '.tour-footer' );
- }
-
- registerEvents() {
- this.taskContent.addEventListener( 'click', ( e ) => {
- if ( e.target.classList.contains( 'prpl-complete-task-btn' ) ) {
- const formData = new FormData(
- this.taskContent.querySelector( 'form' )
- );
- this.formValues = Object.fromEntries( formData.entries() );
- this.complete();
- }
- } );
-
- // Close button handler
- const closeBtn = this.taskContent.querySelector(
- '.prpl-task-close-btn'
- );
- closeBtn?.addEventListener( 'click', () => this.close() );
-
- this.setupFormValidation();
-
- // Initialize upload handling (only if upload field exists)
- this.setupFileUpload();
-
- this.el.addEventListener( 'prplFileUploaded', ( e ) => {
- // Handle file upload for the 'set site icon' task.
- if ( 'core-siteicon' === e.detail.fileInput.dataset.taskId ) {
- // Element which will be used to store the file post ID.
- const nextElementSibling =
- e.detail.fileInput.nextElementSibling;
-
- nextElementSibling.value = e.detail.filePost.id;
-
- // Trigger change so validation is triggered and "Complete" button is enabled.
- nextElementSibling.dispatchEvent(
- new CustomEvent( 'change', {
- bubbles: true,
- } )
- );
- }
- } );
- }
-
- open() {
- if ( this.taskContent ) {
- return; // Already open
- }
-
- // Find the column containing the task list
- const taskList = this.wizard.popover.querySelector( '.prpl-task-list' );
- if ( ! taskList ) {
- return;
- }
-
- const column = taskList.closest( '.prpl-column' );
- if ( ! column ) {
- return;
- }
-
- // Hide the task list
- taskList.style.display = 'none';
-
- // Hide the tour footer (it's part of the step content)
- const tourFooter = this.getTourFooter();
- if ( tourFooter ) {
- tourFooter.style.display = 'none';
- }
-
- // Get task content from template
- const content = this.el
- .querySelector( 'template' )
- .content.cloneNode( true );
-
- // Create task content wrapper
- this.taskContent = document.createElement( 'div' );
- this.taskContent.className = 'prpl-task-content-active';
- this.taskContent.appendChild( content );
-
- // Find the complete button in the form
- const completeBtn = this.taskContent.querySelector(
- '.prpl-complete-task-btn'
- );
-
- if ( completeBtn ) {
- // Create close button
- const closeBtn = document.createElement( 'button' );
- closeBtn.type = 'button';
- closeBtn.className = 'prpl-btn prpl-task-close-btn';
- closeBtn.innerHTML =
- ' ' +
- ProgressPlannerOnboardData.l10n.backToRecommendations;
-
- // Create button wrapper
- const buttonWrapper = document.createElement( 'div' );
- buttonWrapper.className = 'prpl-task-buttons';
-
- // Move complete button into wrapper
- completeBtn.parentNode.insertBefore( buttonWrapper, completeBtn );
- buttonWrapper.appendChild( closeBtn );
- buttonWrapper.appendChild( completeBtn );
- }
-
- // Add task content to the column
- column.appendChild( this.taskContent );
-
- // Hide the popover close button
- const popoverCloseBtn = this.wizard.popover.querySelector(
- '#prpl-tour-close-btn'
- );
- if ( popoverCloseBtn ) {
- popoverCloseBtn.style.display = 'none';
- }
-
- // Register events
- this.registerEvents();
- }
-
- close() {
- if ( ! this.taskContent ) {
- return;
- }
-
- // Remove task content
- this.taskContent.remove();
-
- // Show the task list
- const taskList = this.wizard.popover.querySelector( '.prpl-task-list' );
- if ( taskList ) {
- taskList.style.display = '';
- }
-
- // Show the tour footer (it's part of the step content)
- const tourFooter = this.getTourFooter();
- if ( tourFooter ) {
- tourFooter.style.display = '';
- }
-
- // Show the popover close button
- const popoverCloseBtn = this.wizard.popover.querySelector(
- '#prpl-tour-close-btn'
- );
- if ( popoverCloseBtn ) {
- popoverCloseBtn.style.display = '';
- }
-
- // Clean up
- this.taskContent = null;
- }
-
- complete() {
- ProgressPlannerTourUtils.completeTask( this.id, this.formValues )
- .then( () => {
- this.el.classList.add( 'prpl-task-completed' );
- const taskBtn = this.el.querySelector(
- '.prpl-complete-task-btn'
- );
- if ( taskBtn ) {
- taskBtn.disabled = true;
- }
-
- this.close();
- this.notifyParent();
- } )
- .catch( ( error ) => {
- console.error( error );
- // TODO: Handle error.
- } );
- }
-
- notifyParent() {
- const event = new CustomEvent( 'taskCompleted', {
- bubbles: true,
- detail: { id: this.id, formValues: this.formValues },
- } );
- this.el.dispatchEvent( event );
- }
-
- setupFormValidation() {
- const form = this.taskContent.querySelector( 'form' );
- const submitButton = this.taskContent.querySelector(
- '.prpl-complete-task-btn'
- );
-
- if ( ! form || ! submitButton ) {
- return;
- }
-
- const validateElements = form.querySelectorAll( '[data-validate]' );
- if ( validateElements.length === 0 ) {
- return;
- }
-
- const checkValidation = () => {
- let isValid = true;
-
- validateElements.forEach( ( element ) => {
- const validationType = element.getAttribute( 'data-validate' );
- let elementValid = false;
-
- switch ( validationType ) {
- case 'required':
- elementValid =
- element.value !== null &&
- element.value !== undefined &&
- element.value !== '';
- break;
- case 'not-empty':
- elementValid = element.value.trim() !== '';
- break;
- default:
- elementValid = true;
- }
-
- if ( ! elementValid ) {
- isValid = false;
- }
- } );
-
- submitButton.disabled = ! isValid;
- };
-
- checkValidation();
- validateElements.forEach( ( element ) => {
- element.addEventListener( 'change', checkValidation );
- element.addEventListener( 'input', checkValidation );
- } );
- }
-
- /**
- * Handles drag-and-drop or manual file upload for specific tasks.
- * Only runs if the form contains an upload field.
- */
- setupFileUpload() {
- const uploadContainer = this.taskContent.querySelector(
- '[data-upload-field]'
- );
- if ( ! uploadContainer ) {
- return;
- } // no upload for this task
-
- const fileInput = uploadContainer.querySelector( 'input[type="file"]' );
- const statusDiv = uploadContainer.querySelector(
- '.prpl-upload-status'
- );
-
- // Visual drag behavior
- [ 'dragenter', 'dragover' ].forEach( ( event ) => {
- uploadContainer.addEventListener( event, ( e ) => {
- e.preventDefault();
- uploadContainer.classList.add( 'dragover' );
- } );
- } );
-
- [ 'dragleave', 'drop' ].forEach( ( event ) => {
- uploadContainer.addEventListener( event, ( e ) => {
- e.preventDefault();
- uploadContainer.classList.remove( 'dragover' );
- } );
- } );
-
- uploadContainer.addEventListener( 'drop', ( e ) => {
- const file = e.dataTransfer.files[ 0 ];
- if ( file ) {
- this.uploadFile( file, statusDiv ).then( ( response ) => {
- this.el.dispatchEvent(
- new CustomEvent( 'prplFileUploaded', {
- detail: { file, filePost: response, fileInput },
- bubbles: true,
- } )
- );
- } );
- }
- } );
-
- fileInput?.addEventListener( 'change', ( e ) => {
- const file = e.target.files[ 0 ];
- if ( file ) {
- this.uploadFile( file, statusDiv, fileInput ).then(
- ( response ) => {
- this.el.dispatchEvent(
- new CustomEvent( 'prplFileUploaded', {
- detail: { file, filePost: response, fileInput },
- bubbles: true,
- } )
- );
- }
- );
- }
- } );
-
- // Remove button handler.
- const removeBtn = uploadContainer.querySelector( '.prpl-file-remove-btn' );
- const previewDiv = uploadContainer.querySelector( '.prpl-file-preview' );
- removeBtn?.addEventListener( 'click', () => {
- this.removeUploadedFile( uploadContainer, previewDiv );
- } );
- }
-
- async uploadFile( file, statusDiv ) {
- // Validate file extension
- if ( ! this.isValidFaviconFile( file ) ) {
- const fileInput =
- this.taskContent.querySelector( 'input[type="file"]' );
- const acceptedTypes = fileInput?.accept || 'supported file types';
- statusDiv.textContent = `Invalid file type. Please upload a file with one of these formats: ${ acceptedTypes }`;
- return;
- }
-
- statusDiv.textContent = `Uploading ${ file.name }...`;
-
- const formData = new FormData();
- formData.append( 'file', file );
- formData.append( 'prplFileUpload', '1' );
-
- return fetch( '/wp-json/wp/v2/media', {
- method: 'POST',
- headers: {
- 'X-WP-Nonce': ProgressPlannerOnboardData.nonceWPAPI, // usually wp_localize_script adds this
- },
- body: formData,
- credentials: 'same-origin',
- } )
- .then( ( res ) => {
- if ( 201 !== res.status ) {
- throw new Error( 'Failed to upload file' );
- }
- return res.json();
- } )
- .then( ( response ) => {
- // Testing only, no need to display file name in production.
- // statusDiv.textContent = `${ file.name } uploaded.`;
- statusDiv.style.display = 'none';
-
- // Update the file preview.
- const previewDiv =
- this.taskContent.querySelector( '.prpl-file-preview' );
- if ( previewDiv ) {
- previewDiv.innerHTML = `
`;
- previewDiv.style.display = 'block';
-
- // Add has-image class to drop zone to update styling.
- const dropZone = this.taskContent.querySelector(
- '.prpl-file-drop-zone'
- );
- if ( dropZone ) {
- dropZone.classList.add( 'has-image' );
-
- // Show the remove button.
- const removeBtn = dropZone.querySelector(
- '.prpl-file-remove-btn'
- );
- if ( removeBtn ) {
- removeBtn.hidden = false;
- }
- }
- }
- return response;
- } )
- .catch( ( error ) => {
- console.error( error );
- statusDiv.textContent = `Error: ${ error.message }`;
- } );
- }
-
- /**
- * Validate if file matches the accepted file types from the input
- * @param {File} file The file to validate
- * @return {boolean} True if file extension is supported
- */
- isValidFaviconFile( file ) {
- const fileInput =
- this.taskContent.querySelector( 'input[type="file"]' );
- if ( ! fileInput || ! fileInput.accept ) {
- return true; // No restrictions if no accept attribute
- }
-
- const acceptedTypes = fileInput.accept
- .split( ',' )
- .map( ( type ) => type.trim() );
- const fileName = file.name.toLowerCase();
-
- return acceptedTypes.some( ( type ) => {
- if ( type.startsWith( '.' ) ) {
- // Extension-based validation
- return fileName.endsWith( type );
- } else if ( type.includes( '/' ) ) {
- // MIME type-based validation
- return file.type === type;
- }
- return false;
- } );
- }
-
- /**
- * Remove uploaded file and reset the drop zone state.
- * @param {HTMLElement} dropZone The drop zone element.
- * @param {HTMLElement} previewDiv The preview container element.
- */
- removeUploadedFile( dropZone, previewDiv ) {
- // Clear the preview.
- previewDiv.innerHTML = '';
- previewDiv.style.display = 'none';
-
- // Remove has-image class.
- dropZone.classList.remove( 'has-image' );
-
- // Hide the remove button.
- const removeBtn = dropZone.querySelector( '.prpl-file-remove-btn' );
- if ( removeBtn ) {
- removeBtn.hidden = true;
- }
-
- // Clear the file input.
- const fileInput = dropZone.querySelector( 'input[type="file"]' );
- if ( fileInput ) {
- fileInput.value = '';
- }
-
- // Clear the hidden post_id input and trigger validation.
- const postIdInput = dropZone.querySelector( 'input[name="post_id"]' );
- if ( postIdInput ) {
- postIdInput.value = '';
- postIdInput.dispatchEvent(
- new CustomEvent( 'change', { bubbles: true } )
- );
- }
-
- // Show status div again.
- const statusDiv = dropZone.querySelector( '.prpl-upload-status' );
- if ( statusDiv ) {
- statusDiv.style.display = '';
- statusDiv.textContent = '';
- }
- }
-}
diff --git a/assets/js/onboarding/onboarding.js b/assets/js/onboarding/onboarding.js
deleted file mode 100644
index 3bda0afeb6..0000000000
--- a/assets/js/onboarding/onboarding.js
+++ /dev/null
@@ -1,501 +0,0 @@
-/**
- * Progress Planner Onboarding Wizard
- * Handles the onboarding wizard functionality
- *
- * Dependencies: progress-planner/license-generator
- */
-/* global ProgressPlannerOnboardData */
-
-// eslint-disable-next-line no-unused-vars
-class ProgressPlannerOnboardWizard {
- constructor( config ) {
- this.config = config;
- this.state = {
- currentStep: 0,
- data: {
- moreTasksCompleted: {},
- firstTaskCompleted: false,
- finished: false,
- },
- cleanup: null,
- };
-
- // Store previously focused element for accessibility
- this.previouslyFocusedElement = null;
-
- // Restore saved progress if available
- this.restoreSavedProgress();
-
- // Make state work with reactive updates.
- this.setupStateProxy();
-
- // Set DOM related properties FIRST.
- this.popover = document.getElementById( this.config.popoverId );
- this.contentWrapper = this.popover.querySelector(
- '.tour-content-wrapper'
- );
-
- // Popover buttons.
- this.closeBtn = this.popover.querySelector( '#prpl-tour-close-btn' );
-
- // Initialize tour steps AFTER popover is set
- this.tourSteps = this.initializeTourSteps();
-
- // Setup event listeners after DOM is ready
- this.setupEventListeners();
- }
-
- /**
- * Restore saved progress from server
- */
- restoreSavedProgress() {
- if (
- ! this.config.savedProgress ||
- typeof this.config.savedProgress !== 'object'
- ) {
- return;
- }
-
- const savedState = this.config.savedProgress;
-
- // Restore currentStep if valid
- if (
- typeof savedState.currentStep === 'number' &&
- savedState.currentStep >= 0
- ) {
- this.state.currentStep = savedState.currentStep;
- console.log(
- 'Restored onboarding progress to step:',
- this.state.currentStep
- );
- }
-
- // Restore data object if present
- if ( savedState.data && typeof savedState.data === 'object' ) {
- // Merge saved data with default state
- this.state.data = {
- ...this.state.data,
- ...savedState.data,
- };
-
- // Ensure moreTasksCompleted is an object
- if (
- ! this.state.data.moreTasksCompleted ||
- typeof this.state.data.moreTasksCompleted !== 'object'
- ) {
- this.state.data.moreTasksCompleted = {};
- }
-
- console.log( 'Restored onboarding data:', this.state.data );
- }
- }
-
- /**
- * Initialize tour steps configuration
- * Creates instances of step components
- */
- initializeTourSteps() {
- // Create instances of step components.
- const steps = this.config.steps.map( ( stepName ) => {
- if (
- window[ `Prpl${ stepName }` ] &&
- typeof window[ `Prpl${ stepName }` ] === 'object'
- ) {
- return window[ `Prpl${ stepName }` ];
- }
-
- console.error(
- `Step class "${ stepName }" not found. Available on window:`,
- Object.keys( window ).filter( ( key ) =>
- key.includes( 'Step' )
- )
- );
-
- return null;
- } );
-
- // Set wizard reference for each step
- steps.forEach( ( step ) => step.setWizard( this ) );
-
- return steps;
- }
-
- /**
- * Render current step
- */
- renderStep() {
- const step = this.tourSteps[ this.state.currentStep ];
-
- // Render step content
- this.contentWrapper.innerHTML = step.render();
-
- // Cleanup previous step
- if ( this.state.cleanup ) {
- this.state.cleanup();
- this.state.cleanup = null;
- }
-
- // Mount current step and store cleanup function
- this.state.cleanup = step.onMount( this.state );
-
- // Setup next button (handled by step now)
- step.setupNextButton();
-
- // Update step indicator
- this.popover.dataset.prplStep = this.state.currentStep;
- this.updateStepNavigation();
- }
-
- /**
- * Update step navigation in left column
- */
- updateStepNavigation() {
- const stepItems = this.popover.querySelectorAll(
- '.prpl-nav-step-item'
- );
- let activeStepTitle = '';
-
- stepItems.forEach( ( item, index ) => {
- const icon = item.querySelector( '.prpl-step-icon' );
- const stepNumber = index + 1;
-
- // Remove all state classes
- item.classList.remove( 'prpl-active', 'prpl-completed' );
-
- // Add appropriate class and update icon
- if ( index < this.state.currentStep ) {
- // Completed step: show checkmark
- item.classList.add( 'prpl-completed' );
- icon.textContent = 'âś“';
- } else if ( index === this.state.currentStep ) {
- // Active step: show number
- item.classList.add( 'prpl-active' );
- icon.textContent = stepNumber;
- activeStepTitle =
- item.querySelector( '.prpl-step-label' ).textContent;
- } else {
- // Future step: show number
- icon.textContent = stepNumber;
- }
- } );
-
- // Update mobile step label
- const mobileStepLabel = this.popover.querySelector(
- '#prpl-onboarding-mobile-step-label'
- );
- if ( mobileStepLabel ) {
- mobileStepLabel.textContent = activeStepTitle;
- }
- }
-
- /**
- * Move to next step
- */
- async nextStep() {
- console.log(
- 'nextStep() called, current step:',
- this.state.currentStep
- );
- const step = this.tourSteps[ this.state.currentStep ];
-
- // Check if user can proceed from current step
- if ( ! step.canProceed( this.state ) ) {
- console.log( 'Cannot proceed - step requirements not met' );
- return;
- }
-
- // Call beforeNextStep if step has it (for async operations like license generation)
- if ( step.beforeNextStep ) {
- try {
- await step.beforeNextStep();
- } catch ( error ) {
- console.error( 'Error in beforeNextStep:', error );
- return; // Don't proceed if beforeNextStep fails
- }
- }
-
- if ( this.state.currentStep < this.tourSteps.length - 1 ) {
- this.state.currentStep++;
- console.log( 'Moving to step:', this.state.currentStep );
- this.saveProgressToServer();
- this.renderStep();
- } else {
- console.log( 'Finishing tour - reached last step' );
- this.state.data.finished = true;
- this.closeTour();
-
- // Redirect to the Progress Planner dashboard
- if (
- this.config.lastStepRedirectUrl &&
- this.config.lastStepRedirectUrl.length > 0
- ) {
- window.location.href = this.config.lastStepRedirectUrl;
- }
- }
- }
-
- /**
- * Move to previous step, currently not used.
- */
- prevStep() {
- if ( this.state.currentStep > 0 ) {
- this.state.currentStep--;
- this.renderStep();
- }
- }
-
- /**
- * Close the tour
- */
- closeTour() {
- if ( this.popover ) {
- this.popover.hidePopover();
- }
- this.saveProgressToServer();
-
- // Cleanup active step
- if ( this.state.cleanup ) {
- this.state.cleanup();
- }
-
- // Reset cleanup
- this.state.cleanup = null;
-
- // Restore focus to previously focused element for accessibility
- if (
- this.previouslyFocusedElement &&
- typeof this.previouslyFocusedElement.focus === 'function'
- ) {
- this.previouslyFocusedElement.focus();
- this.previouslyFocusedElement = null;
- }
- }
-
- /**
- * Start the onboarding
- */
- startOnboarding() {
- if ( this.popover ) {
- // Store currently focused element for accessibility
- this.previouslyFocusedElement =
- this.popover.ownerDocument.activeElement;
-
- this.popover.showPopover();
- this.updateStepNavigation();
- this.renderStep();
-
- // Move focus to popover for keyboard accessibility
- // Use setTimeout to ensure popover is visible before focusing
- setTimeout( () => {
- this.popover.focus();
- }, 0 );
- }
- }
-
- /**
- * Save progress to server
- */
- async saveProgressToServer() {
- try {
- const response = await fetch( this.config.adminAjaxUrl, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- body: new URLSearchParams( {
- state: JSON.stringify( this.state ),
- nonce: this.config.nonceProgressPlanner,
- action: 'progress_planner_onboarding_save_progress',
- } ),
- credentials: 'same-origin',
- } );
-
- if ( ! response.ok ) {
- throw new Error( 'Request failed: ' + response.status );
- }
-
- return response.json();
- } catch ( error ) {
- console.error( 'Failed to save tour progress:', error );
- }
- }
-
- /**
- * Update next button state
- * Delegates to current step's updateNextButton method
- */
- updateNextButton() {
- const step = this.tourSteps[ this.state.currentStep ];
- if ( step && typeof step.updateNextButton === 'function' ) {
- step.updateNextButton();
- }
- }
-
- /**
- * Update DOM, used for reactive updates.
- * All changes which should happen when the state changes should be done here.
- */
- updateDOM() {
- this.updateNextButton();
- }
-
- /**
- * Setup event listeners
- */
- setupEventListeners() {
- console.log( 'Setting up event listeners...' );
- if ( this.popover ) {
- console.log( 'Popover found:', this.popover );
-
- this.popover.addEventListener( 'beforetoggle', ( event ) => {
- if ( event.newState === 'open' ) {
- console.log( 'Tour opened' );
- }
- if ( event.newState === 'closed' ) {
- console.log( 'Tour closed' );
- }
- } );
-
- // Note: nextBtn click handler is now set up in renderStep()
- // since the button is part of the step content
-
- if ( this.closeBtn ) {
- this.closeBtn.addEventListener( 'click', ( e ) => {
- console.log( 'Close button clicked!' );
-
- // Display quit confirmation if on welcome step (since privacy policy is accepted there)
- if ( this.state.currentStep === 0 ) {
- e.preventDefault();
- this.showQuitConfirmation();
- return;
- }
-
- this.state.data.finished =
- this.state.currentStep === this.tourSteps.length - 1;
- this.closeTour();
-
- // If on PP Dashboard page and privacy was accepted during onboarding,
- // refresh the page to properly initialize dashboard components.
- if (
- this.state.data.privacyAccepted &&
- window.location.href.includes(
- 'admin.php?page=progress-planner'
- )
- ) {
- window.location.reload();
- }
- } );
- }
- } else {
- console.error( 'Popover not found!' );
- }
- }
-
- /**
- * Show quit confirmation when trying to close without accepting privacy
- */
- showQuitConfirmation() {
- // Replace content with confirmation message
- const originalContent = this.contentWrapper.innerHTML;
-
- // Get template from DOM
- const template = document.getElementById(
- 'prpl-onboarding-quit-confirmation'
- );
- if ( ! template ) {
- console.error( 'Quit confirmation template not found' );
- return;
- }
-
- this.contentWrapper.innerHTML = template.innerHTML;
-
- // Add event listeners
- const quitYes = this.contentWrapper.querySelector( '#prpl-quit-yes' );
- const quitNo = this.contentWrapper.querySelector( '#prpl-quit-no' );
-
- if ( quitYes ) {
- quitYes.addEventListener( 'click', ( e ) => {
- e.preventDefault();
- this.closeTour();
- } );
- }
-
- if ( quitNo ) {
- quitNo.addEventListener( 'click', ( e ) => {
- e.preventDefault();
- // Restore original content
- this.contentWrapper.innerHTML = originalContent;
-
- // Re-mount the step
- this.renderStep();
- } );
- }
- }
-
- /**
- * Setup state proxy for reactive updates
- */
- setupStateProxy() {
- this.state.data = this.createDeepProxy( this.state.data, () =>
- this.updateDOM()
- );
- }
-
- /**
- * Create deep proxy for nested object changes
- * @param {Object} target
- * @param {Function} callback
- */
- createDeepProxy( target, callback ) {
- // Recursively wrap existing nested objects first
- for ( const key of Object.keys( target ) ) {
- if (
- target[ key ] &&
- typeof target[ key ] === 'object' &&
- ! Array.isArray( target[ key ] )
- ) {
- target[ key ] = this.createDeepProxy( target[ key ], callback );
- }
- }
-
- return new Proxy( target, {
- set: ( obj, prop, value ) => {
- if (
- value &&
- typeof value === 'object' &&
- ! Array.isArray( value )
- ) {
- value = this.createDeepProxy( value, callback );
- }
- obj[ prop ] = value;
- callback();
- return true;
- },
- } );
- }
-}
-
-class ProgressPlannerTourUtils {
- /**
- * Complete a task via AJAX
- * @param {string} taskId
- * @param {Object} formValues
- */
- static async completeTask( taskId, formValues = {} ) {
- const response = await fetch( ProgressPlannerOnboardData.adminAjaxUrl, {
- method: 'POST',
- body: new URLSearchParams( {
- form_values: JSON.stringify( formValues ),
- task_id: taskId,
- nonce: ProgressPlannerOnboardData.nonceProgressPlanner,
- action: 'progress_planner_onboarding_complete_task',
- } ),
- } );
-
- if ( ! response.ok ) {
- throw new Error( 'Request failed: ' + response.status );
- }
-
- return response.json();
- }
-}
diff --git a/assets/js/onboarding/steps/BadgesStep.js b/assets/js/onboarding/steps/BadgesStep.js
deleted file mode 100644
index 516636fadb..0000000000
--- a/assets/js/onboarding/steps/BadgesStep.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Badges step - Explains the badge system to users
- * Simple informational step with no user interaction required
- */
-/* global OnboardingStep */
-
-class PrplBadgesStep extends OnboardingStep {
- constructor() {
- super( {
- templateId: 'onboarding-step-badges',
- } );
- }
-
- /**
- * Mount badges step and lazy-load badge graphic
- * Badge is only loaded after privacy policy is accepted
- * @return {Function} Cleanup function
- */
- onMount() {
- const gaugeElement = document.getElementById( 'prpl-gauge-onboarding' );
-
- if ( ! gaugeElement ) {
- return () => {};
- }
-
- // Create badge element using innerHTML to properly instantiate the custom element
- const badgeId = gaugeElement.getAttribute( 'data-badge-id' );
- const badgeName = gaugeElement.getAttribute( 'data-badge-name' );
- const brandingId = gaugeElement.getAttribute( 'data-branding-id' );
-
- gaugeElement.innerHTML = `
-
- `;
-
- // Increment badge point(s) after badge is loaded
- setTimeout( () => {
- if ( gaugeElement ) {
- // Check if the first task was completed.
- if ( this.wizard.state.data.firstTaskCompleted ) {
- gaugeElement.value += 1;
- }
- }
- }, 1500 );
-
- return () => {};
- }
-
- /**
- * User can always proceed from badges step
- * @return {boolean} Always returns true
- */
- canProceed() {
- return true;
- }
-}
-
-window.PrplBadgesStep = new PrplBadgesStep();
diff --git a/assets/js/onboarding/steps/EmailFrequencyStep.js b/assets/js/onboarding/steps/EmailFrequencyStep.js
deleted file mode 100644
index ff9ffc56ea..0000000000
--- a/assets/js/onboarding/steps/EmailFrequencyStep.js
+++ /dev/null
@@ -1,208 +0,0 @@
-/**
- * Email Frequency step - Allow users to opt in/out of weekly emails
- * If opted in, collects name and email for subscription
- */
-/* global OnboardingStep, ProgressPlannerOnboardData, LicenseGenerator */
-
-class PrplEmailFrequencyStep extends OnboardingStep {
- constructor() {
- super( {
- templateId: 'onboarding-step-email-frequency',
- } );
- }
-
- /**
- * Mount the email frequency step
- * Sets up radio button and form field listeners
- * @param {Object} state - Wizard state
- * @return {Function} Cleanup function
- */
- onMount( state ) {
- const emailWeeklyRadio =
- this.popover.querySelector( '#prpl-email-weekly' );
- const dontEmailRadio = this.popover.querySelector( '#prpl-dont-email' );
- const emailForm = this.popover.querySelector( '#prpl-email-form' );
- const nameInput = this.popover.querySelector( '#prpl-email-name' );
- const emailInput = this.popover.querySelector( '#prpl-email-address' );
-
- if ( ! emailWeeklyRadio || ! dontEmailRadio || ! emailForm ) {
- return () => {};
- }
-
- // Initialize state
- if ( ! state.data.emailFrequency ) {
- state.data.emailFrequency = {
- choice: 'weekly', // Default to 'weekly'
- name: nameInput ? nameInput.value.trim() : '', // Get pre-populated value
- email: emailInput ? emailInput.value.trim() : '', // Get pre-populated value
- };
- }
-
- // Set radio button state from wizard state
- if ( state.data.emailFrequency.choice === 'weekly' ) {
- emailWeeklyRadio.checked = true;
- emailForm.style.display = 'block';
- } else if ( state.data.emailFrequency.choice === 'none' ) {
- dontEmailRadio.checked = true;
- emailForm.style.display = 'none';
- }
-
- // Set form values from state (or keep pre-populated values)
- if ( nameInput ) {
- nameInput.value = state.data.emailFrequency.name || nameInput.value;
- }
- if ( emailInput ) {
- emailInput.value =
- state.data.emailFrequency.email || emailInput.value;
- }
-
- // Radio button change handlers
- const weeklyHandler = ( e ) => {
- if ( e.target.checked ) {
- state.data.emailFrequency.choice = 'weekly';
- emailForm.style.display = 'block';
-
- // Update button state
- this.updateNextButton();
- }
- };
-
- const dontEmailHandler = ( e ) => {
- if ( e.target.checked ) {
- state.data.emailFrequency.choice = 'none';
- emailForm.style.display = 'none';
-
- // Update button state
- this.updateNextButton();
- }
- };
-
- // Form input handlers
- const nameHandler = ( e ) => {
- state.data.emailFrequency.name = e.target.value.trim();
- this.updateNextButton();
- };
-
- const emailHandler = ( e ) => {
- state.data.emailFrequency.email = e.target.value.trim();
- this.updateNextButton();
- };
-
- // Add event listeners
- emailWeeklyRadio.addEventListener( 'change', weeklyHandler );
- dontEmailRadio.addEventListener( 'change', dontEmailHandler );
-
- if ( nameInput ) {
- nameInput.addEventListener( 'input', nameHandler );
- }
- if ( emailInput ) {
- emailInput.addEventListener( 'input', emailHandler );
- }
-
- // Cleanup function
- return () => {
- emailWeeklyRadio.removeEventListener( 'change', weeklyHandler );
- dontEmailRadio.removeEventListener( 'change', dontEmailHandler );
-
- if ( nameInput ) {
- nameInput.removeEventListener( 'input', nameHandler );
- }
- if ( emailInput ) {
- emailInput.removeEventListener( 'input', emailHandler );
- }
- };
- }
-
- /**
- * User can proceed if:
- * - "Don't email me" is selected, OR
- * - "Email me weekly" is selected AND both name and email fields are filled
- * @param {Object} state - Wizard state
- * @return {boolean} True if can proceed
- */
- canProceed( state ) {
- // Initialize state if needed (defensive check)
- if ( ! state.data.emailFrequency ) {
- state.data.emailFrequency = {
- choice: null,
- name: '',
- email: '',
- };
- }
-
- const emailFrequency = state.data.emailFrequency;
-
- if ( ! emailFrequency.choice ) {
- return false;
- }
-
- // If user chose "don't email", they can proceed immediately
- if ( emailFrequency.choice === 'none' ) {
- return true;
- }
-
- // If user chose "weekly", check that name and email are filled
- if ( emailFrequency.choice === 'weekly' ) {
- return !! ( emailFrequency.name && emailFrequency.email );
- }
-
- return false;
- }
-
- /**
- * Called before advancing to next step
- * Fires AJAX request to subscribe user if "Email me weekly" was selected
- * @return {Promise} Resolves when action is complete
- */
- async beforeNextStep() {
- const state = this.getState();
-
- // Only send AJAX if user chose to receive emails
- if ( state.data.emailFrequency.choice !== 'weekly' ) {
- return Promise.resolve();
- }
-
- // Show spinner
- const spinner = this.showSpinner( this.nextBtn );
-
- try {
- // Use LicenseGenerator to handle the license generation process
- await LicenseGenerator.generateLicense(
- {
- name: state.data.emailFrequency.name,
- email: state.data.emailFrequency.email,
- site: ProgressPlannerOnboardData.site,
- timezone_offset: ProgressPlannerOnboardData.timezone_offset,
- 'with-email': 'yes',
- },
- {
- onboardNonceURL: ProgressPlannerOnboardData.onboardNonceURL,
- onboardAPIUrl: ProgressPlannerOnboardData.onboardAPIUrl,
- adminAjaxUrl: ProgressPlannerOnboardData.adminAjaxUrl,
- nonce: ProgressPlannerOnboardData.nonceProgressPlanner,
- }
- );
-
- console.log( 'Successfully subscribed' );
- } catch ( error ) {
- console.error( 'Failed to subscribe:', error );
-
- // Display error message to user
- this.showErrorMessage(
- error.message || 'Failed to subscribe. Please try again.',
- 'Error subscribing'
- );
-
- // Re-enable the button so user can retry
- this.setNextButtonDisabled( false );
-
- // Don't proceed to next step
- throw error;
- } finally {
- // Remove spinner
- spinner.remove();
- }
- }
-}
-
-window.PrplEmailFrequencyStep = new PrplEmailFrequencyStep();
diff --git a/assets/js/onboarding/steps/FirstTaskStep.js b/assets/js/onboarding/steps/FirstTaskStep.js
deleted file mode 100644
index 633f109432..0000000000
--- a/assets/js/onboarding/steps/FirstTaskStep.js
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * First Task step - User completes their first task
- * Handles task completion and form submission
- */
-/* global OnboardingStep, ProgressPlannerTourUtils */
-class PrplFirstTaskStep extends OnboardingStep {
- constructor() {
- super( {
- templateId: 'onboarding-step-first-task',
- } );
- }
-
- /**
- * Mount the first task step
- * Sets up event listener for task completion
- * @param {Object} state - Wizard state
- * @return {Function} Cleanup function
- */
- onMount( state ) {
- const btn = this.popover.querySelector( '.prpl-complete-task-btn' );
- if ( ! btn ) {
- return () => {};
- }
-
- const handler = ( e ) => {
- const thisBtn = e.target.closest( 'button' );
- const form = thisBtn.closest( 'form' );
- let formValues = {};
-
- if ( form ) {
- const formData = new FormData( form );
- formValues = Object.fromEntries( formData.entries() );
- }
-
- ProgressPlannerTourUtils.completeTask(
- thisBtn.dataset.taskId,
- formValues
- )
- .then( () => {
- thisBtn.classList.add( 'prpl-complete-task-btn-completed' );
- this.updateState( 'firstTaskCompleted', {
- [ thisBtn.dataset.taskId ]: true,
- } );
-
- // Automatically advance to the next step
- this.nextStep();
- } )
- .catch( ( error ) => {
- console.error( error );
- thisBtn.classList.add( 'prpl-complete-task-btn-error' );
- } );
- };
-
- btn.addEventListener( 'click', handler );
- return () => btn.removeEventListener( 'click', handler );
- }
-
- /**
- * User can only proceed if they've completed the first task
- * @param {Object} state - Wizard state
- * @return {boolean} True if first task is completed
- */
- canProceed( state ) {
- return !! state.data.firstTaskCompleted;
- }
-}
-
-window.PrplFirstTaskStep = new PrplFirstTaskStep();
diff --git a/assets/js/onboarding/steps/MoreTasksStep.js b/assets/js/onboarding/steps/MoreTasksStep.js
deleted file mode 100644
index 998d943ade..0000000000
--- a/assets/js/onboarding/steps/MoreTasksStep.js
+++ /dev/null
@@ -1,143 +0,0 @@
-/**
- * More Tasks step - User completes additional tasks
- * Handles multiple tasks that can be completed in any order
- * Each task may open a sub-popover with its own form
- * Split into 2 substeps: intro screen and task list
- */
-/* global OnboardingStep, PrplOnboardTask */
-
-class PrplMoreTasksStep extends OnboardingStep {
- subSteps = [ 'more-tasks-intro', 'more-tasks-tasks' ];
-
- constructor() {
- super( {
- templateId: 'onboarding-step-more-tasks',
- } );
- this.tasks = [];
- this.currentSubStep = 0;
- }
-
- /**
- * Mount the more tasks step
- * Initializes all tasks and sets up event listeners
- * @param {Object} state - Wizard state
- * @return {Function} Cleanup function
- */
- onMount( state ) {
- // Always start from first sub-step
- this.currentSubStep = 0;
-
- // Hide footer initially (will show on tasks substep)
- this.toggleStepFooter( false );
-
- // Render the current sub-step
- this.renderSubStep( state );
-
- // Setup continue button listener
- const continueBtn = this.popover.querySelector(
- '.prpl-more-tasks-continue'
- );
- if ( continueBtn ) {
- continueBtn.addEventListener( 'click', () => {
- this.advanceSubStep( state );
- } );
- }
-
- // Setup finish onboarding link in intro
- const finishLink = this.popover.querySelector(
- '.prpl-more-tasks-substep[data-substep="intro"] .prpl-finish-onboarding'
- );
- if ( finishLink ) {
- finishLink.addEventListener( 'click', ( e ) => {
- e.preventDefault();
- this.wizard.finishOnboarding();
- } );
- }
-
- // Initialize task completion tracking
- const moreTasks = this.popover.querySelectorAll(
- '.prpl-task-item[data-task-id]'
- );
- moreTasks.forEach( ( btn ) => {
- if ( ! state.data.moreTasksCompleted ) {
- state.data.moreTasksCompleted = {};
- }
- state.data.moreTasksCompleted[ btn.dataset.taskId ] = false;
- } );
-
- // Initialize PrplOnboardTask instances for each task, passing wizard reference
- this.tasks = Array.from(
- this.popover.querySelectorAll( '[data-popover="task"]' )
- ).map( ( t ) => new PrplOnboardTask( t, this.wizard ) );
-
- // Listen for task completion events
- const handler = ( e ) => {
- // Update state when a task is completed
- state.data.moreTasksCompleted[ e.detail.id ] = true;
-
- // Update next button state
- this.updateNextButton();
- };
-
- this.popover.addEventListener( 'taskCompleted', handler );
-
- // Return cleanup function
- return () => {
- this.popover.removeEventListener( 'taskCompleted', handler );
- // Clean up task instances
- this.tasks = [];
- // Show footer when leaving this step
- this.toggleStepFooter( true );
- };
- }
-
- /**
- * Render the current sub-step
- * @param {Object} state - Wizard state
- */
- renderSubStep( state ) {
- const subStepName = this.subSteps[ this.currentSubStep ];
-
- // Show/hide sub-step containers
- this.subSteps.forEach( ( step ) => {
- const container = this.popover.querySelector(
- `.prpl-more-tasks-substep[data-substep="${ step }"]`
- );
- if ( container ) {
- container.style.display = step === subStepName ? '' : 'none';
- }
- } );
-
- // Show footer only on tasks substep
- const isTasksSubStep = subStepName === 'more-tasks-tasks';
- this.toggleStepFooter( isTasksSubStep );
-
- // Update Next button state if on tasks sub-step
- if ( isTasksSubStep ) {
- this.updateNextButton();
- }
- }
-
- /**
- * Advance to next sub-step
- * @param {Object} state - Wizard state
- */
- advanceSubStep( state ) {
- if ( this.currentSubStep < this.subSteps.length - 1 ) {
- this.currentSubStep++;
- this.renderSubStep( state );
- }
- }
-
- /**
- * User can only proceed if on tasks substep
- * @param {Object} state - Wizard state
- * @return {boolean} True if can proceed
- */
- canProceed( state ) {
- // Can only proceed if on tasks substep
- return this.currentSubStep === this.subSteps.length - 1;
- }
-}
-
-window.PrplMoreTasksStep = new PrplMoreTasksStep();
diff --git a/assets/js/onboarding/steps/OnboardingStep.js b/assets/js/onboarding/steps/OnboardingStep.js
deleted file mode 100644
index 14a9c8f5ee..0000000000
--- a/assets/js/onboarding/steps/OnboardingStep.js
+++ /dev/null
@@ -1,326 +0,0 @@
-/**
- * Base class for onboarding steps
- * All step components should extend this class
- */
-class OnboardingStep {
- /**
- * Constructor
- * @param {Object} config - Step configuration
- * @param {string} config.id - Unique step identifier
- * @param {string} config.templateId - ID of the template element containing the step HTML
- */
- constructor( config ) {
- this.templateId = config.templateId;
- this.wizard = null; // Reference to parent wizard
- this.popover = null; // Reference to popover element
- this.cleanup = null; // Cleanup function for event listeners
- this.nextBtn = null; // Reference to next button element
- }
-
- /**
- * Set wizard reference
- * @param {ProgressPlannerOnboardWizard} wizard
- */
- setWizard( wizard ) {
- this.wizard = wizard;
- this.popover = wizard.popover;
- }
-
- /**
- * Get the step's HTML content
- * @return {string} HTML content
- */
- render() {
- const template = document.getElementById( this.templateId );
- if ( ! template ) {
- console.error( `Template not found: ${ this.templateId }` );
- return '';
- }
- return template.innerHTML;
- }
-
- /**
- * Called when step is mounted to DOM
- * Override this method to setup event listeners and step-specific logic
- * @param {Object} state - Wizard state
- * @return {Function} Cleanup function to be called when step unmounts
- */
- onMount( state ) {
- // Override in subclass
- return () => {};
- }
-
- /**
- * Check if user can proceed to next step
- * Override this method to add step-specific validation
- * @param {Object} state - Wizard state
- * @return {boolean} True if user can proceed
- */
- canProceed( state ) {
- // Override in subclass
- return true;
- }
-
- /**
- * Called when step is about to be unmounted
- * Override this method for cleanup logic
- */
- onUnmount() {
- if ( this.cleanup ) {
- this.cleanup();
- this.cleanup = null;
- }
- }
-
- /**
- * Utility method to update wizard state
- * @param {string} key - State key to update
- * @param {*} value - New value
- */
- updateState( key, value ) {
- if ( this.wizard ) {
- this.wizard.state.data[ key ] = value;
- }
- }
-
- /**
- * Utility method to get current state
- * @return {Object} Current wizard state
- */
- getState() {
- return this.wizard ? this.wizard.state : null;
- }
-
- /**
- * Utility method to advance to next step
- */
- nextStep() {
- if ( this.wizard ) {
- this.wizard.nextStep();
- }
- }
-
- /**
- * Get the tour footer element
- * @return {HTMLElement|null} The tour footer element or null if not found
- */
- getTourFooter() {
- return this.wizard?.contentWrapper?.querySelector( '.tour-footer' );
- }
-
- /**
- * Show error message to user
- * @param {string} message Error message to display
- * @param {string} title Optional error title
- */
- showErrorMessage( message, title = '' ) {
- // Remove existing error if any
- this.clearErrorMessage();
-
- // Build title HTML if provided
- const titleHtml = title ? `
${ this.escapeHtml( title ) }
` : '';
-
- // Get error icon from wizard config
- const errorIcon = this.wizard?.config?.errorIcon || '';
-
- // Create error message element
- const errorDiv = document.createElement( 'div' );
- errorDiv.className = 'prpl-error-message';
- errorDiv.innerHTML = `
-
-
- ${ errorIcon }
-
-
- ${ titleHtml }
-
${ this.escapeHtml( message ) }
-
-
- `;
-
- // Add error message to tour footer
- const footer = this.getTourFooter();
- if ( footer ) {
- footer.prepend( errorDiv );
- }
- }
-
- /**
- * Clear error message
- */
- clearErrorMessage() {
- const existingError = this.wizard?.popover?.querySelector(
- '.prpl-error-message'
- );
- if ( existingError ) {
- existingError.remove();
- }
- }
-
- /**
- * Escape HTML to prevent XSS
- * @param {string} text Text to escape
- * @return {string} Escaped text
- */
- escapeHtml( text ) {
- const div = document.createElement( 'div' );
- div.textContent = text;
- return div.innerHTML;
- }
-
- /**
- * Show spinner before a button and disable the button
- * @param {HTMLElement} button Button element to show spinner before and disable
- * @return {HTMLElement} The created spinner element
- */
- showSpinner( button ) {
- const spinner = document.createElement( 'span' );
- spinner.classList.add( 'prpl-spinner' );
- spinner.innerHTML =
- '';
-
- button.parentElement.insertBefore( spinner, button );
- button.disabled = true;
-
- return spinner;
- }
-
- /**
- * Toggle visibility of the footer in this step's template
- * @param {boolean} visible - Whether to show the footer
- */
- toggleStepFooter( visible ) {
- const stepFooter = this.getTourFooter();
- if ( stepFooter ) {
- stepFooter.style.display = visible ? 'flex' : 'none';
- }
- }
-
- /**
- * Called before advancing to next step
- * Fires AJAX request to subscribe user if "Email me weekly" was selected
- * @return {Promise} Resolves when action is complete
- */
- async beforeNextStep() {
- // Override in subclass
- return Promise.resolve();
- }
-
- /**
- * Setup next button after step is rendered
- * Finds button, attaches click handler, and initializes state
- * Called automatically by wizard after rendering
- */
- setupNextButton() {
- // Find the next button in the rendered step content
- this.nextBtn =
- this.wizard?.contentWrapper?.querySelector( '.prpl-tour-next' );
-
- if ( ! this.nextBtn ) {
- // Step doesn't have a next button (e.g., SettingsStep with sub-steps)
- return;
- }
-
- // Remove any existing listeners by cloning the button
- const newBtn = this.nextBtn.cloneNode( true );
- if ( this.nextBtn.parentNode ) {
- this.nextBtn.parentNode.replaceChild( newBtn, this.nextBtn );
- }
- this.nextBtn = newBtn;
-
- // Add click listener
- this.nextBtn.addEventListener( 'click', () => {
- console.log( 'Next button clicked!' );
- this.nextStep();
- } );
-
- // Initialize button state
- this.updateNextButton();
-
- // Call hook for subclasses to add custom button behavior
- // Returns optional cleanup function
- const customCleanup = this.onNextButtonSetup();
-
- // If step provided a cleanup function, chain it with existing cleanup
- if ( customCleanup && typeof customCleanup === 'function' ) {
- const originalCleanup = this.cleanup;
- this.cleanup = () => {
- customCleanup();
- if ( originalCleanup ) {
- originalCleanup();
- }
- };
- }
- }
-
- /**
- * Called after next button is setup
- * Override to add custom button behavior
- * @return {Function|void} Optional cleanup function
- */
- onNextButtonSetup() {
- // Override in subclass
- // Return a cleanup function if you need to remove event listeners
- }
-
- /**
- * Update next button state (text and enabled/disabled)
- * Called when step state changes
- */
- updateNextButton() {
- if ( ! this.nextBtn ) {
- return;
- }
-
- const state = this.getState();
- const canProceed = this.canProceed( state );
-
- // Update enabled/disabled state
- this.setNextButtonDisabled( ! canProceed );
-
- // Update button text
- this.updateNextButtonText();
- }
-
- /**
- * Update next button text based on step configuration and wizard state
- * Currently this is only used to change button text on the last step to "Take me to the Recommendations dashboard"
- */
- updateNextButtonText() {
- if ( ! this.nextBtn || ! this.wizard ) {
- return;
- }
-
- const isLastStep =
- this.wizard.state.currentStep === this.wizard.tourSteps.length - 1;
-
- // Check if step provides custom button text
- if ( isLastStep ) {
- // On last step, use "Take me to the Recommendations dashboard" text
- const dashboardText =
- this.wizard.config?.l10n?.dashboard ||
- 'Take me to the Recommendations dashboard';
- this.nextBtn.innerHTML = dashboardText;
- }
- }
-
- /**
- * Enable or disable the next button
- * Separated into its own method for easy customization
- * @param {boolean} disabled - Whether to disable the button
- */
- setNextButtonDisabled( disabled ) {
- if ( ! this.nextBtn ) {
- return;
- }
-
- // Using prpl-btn-disabled CSS class instead of the disabled attribute
- if ( disabled ) {
- this.nextBtn.classList.add( 'prpl-btn-disabled' );
- this.nextBtn.setAttribute( 'aria-disabled', 'true' );
- } else {
- this.nextBtn.classList.remove( 'prpl-btn-disabled' );
- this.nextBtn.setAttribute( 'aria-disabled', 'false' );
- }
- }
-}
diff --git a/assets/js/onboarding/steps/SettingsStep.js b/assets/js/onboarding/steps/SettingsStep.js
deleted file mode 100644
index 8986db6e74..0000000000
--- a/assets/js/onboarding/steps/SettingsStep.js
+++ /dev/null
@@ -1,492 +0,0 @@
-/**
- * Settings step - Configure About, Contact, FAQ pages, and Post Types
- * Multi-step process with 5 sub-steps
- */
-/* global OnboardingStep, ProgressPlannerOnboardData */
-
-class PrplSettingsStep extends OnboardingStep {
- subSteps = [ 'homepage', 'about', 'contact', 'faq', 'post-types' ];
-
- defaultSettings = {
- homepage: {
- hasPage: true, // true if checkbox is NOT checked (default: unchecked)
- pageId: null,
- },
- about: {
- hasPage: true, // true if checkbox is NOT checked (default: unchecked)
- pageId: null,
- },
- contact: {
- hasPage: true,
- pageId: null,
- },
- faq: {
- hasPage: true,
- pageId: null,
- },
- 'post-types': {
- selectedTypes: [], // Array of selected post type slugs
- },
- };
-
- constructor() {
- super( {
- templateId: 'onboarding-step-settings',
- } );
- this.currentSubStep = 0;
- }
-
- /**
- * Mount the settings step
- * Sets up event listeners for page select and save button
- * @param {Object} state - Wizard state
- * @return {Function} Cleanup function
- */
- onMount( state ) {
- // Initialize state
- if ( ! state.data.settings ) {
- state.data.settings = {};
- }
-
- // Initialize missing sub-steps
- for ( const [ key, defaultValue ] of Object.entries(
- this.defaultSettings
- ) ) {
- if ( ! state.data.settings[ key ] ) {
- state.data.settings[ key ] = { ...defaultValue };
- }
- }
-
- // Always start from first sub-step
- this.currentSubStep = 0;
-
- // Hide footer in step template initially (will show on last sub-step)
- this.toggleStepFooter( false );
-
- // Render the current sub-step
- this.renderSubStep( state );
-
- // Return cleanup function
- return () => {
- // Show footer when leaving this step (for other steps that might need it)
- this.toggleStepFooter( true );
- };
- }
-
- /**
- * Render the current sub-step
- * @param {Object} state - Wizard state
- */
- renderSubStep( state ) {
- const subStepName = this.subSteps[ this.currentSubStep ];
- const subStepData = state.data.settings[ subStepName ];
-
- // Update progress indicator
- /*
- const progressIndicator = this.popover.querySelector(
- '.prpl-settings-progress'
- );
- if ( progressIndicator ) {
- progressIndicator.textContent = `${ this.currentSubStep + 1 }/${
- this.subSteps.length
- }`;
- }
- */
-
- // Show/hide sub-step containers
- this.subSteps.forEach( ( step, index ) => {
- const container = this.popover.querySelector(
- `.prpl-setting-item[data-page="${ step }"]`
- );
- if ( container ) {
- container.style.display =
- index === this.currentSubStep ? 'flex' : 'none';
- }
- } );
-
- // Hide "Save setting" button on last sub-step (show Next/Dashboard instead)
- const isLastSubStep = this.currentSubStep === this.subSteps.length - 1;
- const saveButton = this.popover.querySelector(
- `#prpl-save-${ subStepName }-setting`
- );
- if ( saveButton ) {
- saveButton.style.display = isLastSubStep ? 'none' : '';
- }
-
- // Setup event listeners for current sub-step
- this.setupSubStepListeners( subStepName, subStepData, state );
-
- // Show/hide footer based on sub-step
- this.toggleStepFooter( isLastSubStep );
-
- // Update Next/Dashboard button state if on last sub-step
- if ( isLastSubStep ) {
- this.updateNextButton();
- }
- }
-
- /**
- * Setup event listeners for a sub-step
- * @param {string} subStepName - Name of sub-step (about/contact/faq/post-types)
- * @param {Object} subStepData - Data for this sub-step
- * @param {Object} state - Wizard state
- */
- setupSubStepListeners( subStepName, subStepData, state ) {
- // Handle page selection sub-steps (about, contact, faq)
- if (
- [ 'homepage', 'about', 'contact', 'faq' ].includes( subStepName )
- ) {
- this.setupPageSelectListeners( subStepName, subStepData, state );
- return;
- }
-
- // Handle post types sub-step
- if ( subStepName === 'post-types' ) {
- this.setupPostTypesListeners( subStepName, subStepData, state );
- }
- }
-
- /**
- * Setup event listeners for page select sub-steps (about, contact, faq)
- * @param {string} subStepName - Name of sub-step
- * @param {Object} subStepData - Data for this sub-step
- * @param {Object} state - Wizard state
- */
- setupPageSelectListeners( subStepName, subStepData, state ) {
- // Get select and checkbox
- const pageSelect = this.popover.querySelector(
- `select[name="pages[${ subStepName }][id]"]`
- );
- const noPageCheckbox = this.popover.querySelector(
- `#prpl-no-${ subStepName }-page`
- );
-
- // Get save button
- const saveButton = this.popover.querySelector(
- `#prpl-save-${ subStepName }-setting`
- );
-
- if ( ! pageSelect || ! noPageCheckbox || ! saveButton ) {
- return;
- }
-
- // Get select wrapper
- const selectWrapper = this.popover.querySelector(
- `.prpl-setting-item[data-page="${ subStepName }"] .prpl-select-page`
- );
-
- // Set initial states from saved data
- if ( subStepData.pageId ) {
- pageSelect.value = subStepData.pageId;
- }
-
- if ( ! subStepData.hasPage ) {
- noPageCheckbox.checked = true;
- if ( selectWrapper ) {
- selectWrapper.classList.add( 'prpl-disabled' );
- }
- }
-
- // Page select handler
- pageSelect.addEventListener( 'change', ( e ) => {
- subStepData.pageId = e.target.value;
- this.updateSaveButtonState( saveButton, subStepData );
-
- // Update Next/Dashboard button if on last sub-step
- if ( this.currentSubStep === this.subSteps.length - 1 ) {
- this.updateNextButton();
- }
- } );
-
- // Checkbox handler
- noPageCheckbox.addEventListener( 'change', ( e ) => {
- subStepData.hasPage = ! e.target.checked;
-
- // Display the note if the checkbox is checked.
- const note = this.popover.querySelector(
- `.prpl-setting-item[data-page="${ subStepName }"] .prpl-setting-footer .prpl-setting-note`
- );
-
- // Hide/show select based on checkbox
- if ( e.target.checked ) {
- // Checkbox is checked - hide select
- if ( selectWrapper ) {
- selectWrapper.classList.add( 'prpl-disabled' );
- }
- pageSelect.value = ''; // Reset selection
- subStepData.pageId = null;
- if ( note ) {
- note.style.display = 'flex';
- }
- } else if ( selectWrapper ) {
- // Checkbox is unchecked - show select
- if ( selectWrapper ) {
- selectWrapper.classList.remove( 'prpl-disabled' );
- }
-
- if ( note ) {
- note.style.display = 'none';
- }
- }
-
- this.updateSaveButtonState( saveButton, subStepData );
-
- // Update Next/Dashboard button if on last sub-step
- if ( this.currentSubStep === this.subSteps.length - 1 ) {
- this.updateNextButton();
- }
- } );
-
- // Save button handler - just advances to next sub-step
- saveButton.addEventListener( 'click', () => {
- this.advanceSubStep( state );
- } );
-
- // Initial button state
- this.updateSaveButtonState( saveButton, subStepData );
- }
-
- /**
- * Setup event listeners for post types sub-step
- * @param {string} subStepName - Name of sub-step
- * @param {Object} subStepData - Data for this sub-step
- * @param {Object} state - Wizard state
- */
- setupPostTypesListeners( subStepName, subStepData, state ) {
- const container = this.popover.querySelector(
- `.prpl-setting-item[data-page="${ subStepName }"]`
- );
- const saveButton = this.popover.querySelector(
- `#prpl-save-${ subStepName }-setting`
- );
-
- if ( ! container || ! saveButton ) {
- return;
- }
-
- // Get all checkboxes
- const checkboxes = container.querySelectorAll(
- 'input[type="checkbox"][name="prpl-post-types-include[]"]'
- );
-
- // Initialize selected types from checkboxes that are already checked (from template)
- // or from saved data if available
- if (
- subStepData.selectedTypes &&
- subStepData.selectedTypes.length > 0
- ) {
- // Use saved data if available
- checkboxes.forEach( ( checkbox ) => {
- checkbox.checked = subStepData.selectedTypes.includes(
- checkbox.value
- );
- } );
- } else {
- // Initialize from checkboxes that are already checked in the template
- subStepData.selectedTypes = Array.from( checkboxes )
- .filter( ( cb ) => cb.checked )
- .map( ( cb ) => cb.value );
-
- // If no checkboxes are checked, default to all checked
- if ( subStepData.selectedTypes.length === 0 ) {
- checkboxes.forEach( ( checkbox ) => {
- checkbox.checked = true;
- subStepData.selectedTypes.push( checkbox.value );
- } );
- }
- }
-
- // Add change listeners to checkboxes
- checkboxes.forEach( ( checkbox ) => {
- checkbox.addEventListener( 'change', () => {
- // Update selected types array
- subStepData.selectedTypes = Array.from( checkboxes )
- .filter( ( cb ) => cb.checked )
- .map( ( cb ) => cb.value );
-
- this.updateSaveButtonState( saveButton, subStepData );
-
- // Update Next/Dashboard button if on last sub-step
- if ( this.currentSubStep === this.subSteps.length - 1 ) {
- this.updateNextButton();
- }
- } );
- } );
-
- // Save button handler - just advances to next sub-step
- saveButton.addEventListener( 'click', () => {
- this.advanceSubStep( state );
- } );
-
- // Initial button state
- this.updateSaveButtonState( saveButton, subStepData );
- }
-
- /**
- * Advance to next sub-step
- * @param {Object} state - Wizard state
- */
- advanceSubStep( state ) {
- if ( this.currentSubStep < this.subSteps.length - 1 ) {
- this.currentSubStep++;
- this.renderSubStep( state );
- // Footer visibility is handled in renderSubStep()
- }
- }
-
- /**
- * Update save button state
- * @param {HTMLElement} button - Save button element
- * @param {Object} subStepData - Sub-step data
- */
- updateSaveButtonState( button, subStepData ) {
- const canSave = this.canSaveSubStep( subStepData );
- button.disabled = ! canSave;
- }
-
- /**
- * Check if sub-step can be saved
- * @param {Object} subStepData - Sub-step data
- * @return {boolean} True if can save
- */
- canSaveSubStep( subStepData ) {
- // Handle page selection sub-steps (about, contact, faq)
- if ( subStepData.hasPage !== undefined ) {
- // If user has the page, they must select one
- if ( subStepData.hasPage && ! subStepData.pageId ) {
- return false;
- }
-
- // If checkbox is checked (don't have page), can save
- if ( ! subStepData.hasPage ) {
- return true;
- }
-
- // If page is selected, can save
- return !! subStepData.pageId;
- }
-
- // Handle post types sub-step - at least one must be selected
- if ( subStepData.selectedTypes !== undefined ) {
- return subStepData.selectedTypes.length > 0;
- }
-
- return false;
- }
-
- /**
- * User can proceed if on last sub-step and it's valid
- * @param {Object} state - Wizard state
- * @return {boolean} True if can proceed
- */
- canProceed( state ) {
- if ( ! state.data.settings ) {
- return false;
- }
-
- // Can only proceed if on last sub-step
- if ( this.currentSubStep !== this.subSteps.length - 1 ) {
- return false;
- }
-
- // Check if all sub-steps have valid data
- return this.subSteps.every( ( step ) => {
- const subStepData = state.data.settings[ step ];
- return this.canSaveSubStep( subStepData );
- } );
- }
-
- /**
- * Called before advancing to next step
- * Saves all settings via AJAX
- * @return {Promise} Resolves when settings are saved
- */
- async beforeNextStep() {
- const state = this.getState();
-
- // Show spinner on Next button
- const spinner = this.showSpinner( this.nextBtn );
-
- try {
- // Collect all settings data for a single AJAX request
- const formDataObj = new FormData();
- formDataObj.append( 'action', 'prpl_save_all_onboarding_settings' );
- formDataObj.append(
- 'nonce',
- ProgressPlannerOnboardData.nonceProgressPlanner
- );
-
- // Collect page settings (about, contact, faq)
- const pages = {};
- for ( const subStepName of this.subSteps ) {
- const subStepData = state.data.settings[ subStepName ];
-
- if (
- [ 'homepage', 'about', 'contact', 'faq' ].includes(
- subStepName
- )
- ) {
- pages[ subStepName ] = {
- id: subStepData.pageId || '',
- have_page: subStepData.hasPage ? 'yes' : 'no',
- };
- }
- }
-
- // Add pages data as JSON
- if ( Object.keys( pages ).length > 0 ) {
- formDataObj.append( 'pages', JSON.stringify( pages ) );
- }
-
- // Add post types
- const postTypesData = state.data.settings[ 'post-types' ];
- if ( postTypesData && postTypesData.selectedTypes ) {
- postTypesData.selectedTypes.forEach( ( postType ) => {
- formDataObj.append( 'prpl-post-types-include[]', postType );
- } );
- }
-
- // Send single AJAX request
- const response = await fetch(
- ProgressPlannerOnboardData.adminAjaxUrl,
- {
- method: 'POST',
- body: formDataObj,
- credentials: 'same-origin',
- }
- );
-
- if ( ! response.ok ) {
- throw new Error( 'Request failed: ' + response.status );
- }
-
- const result = await response.json();
-
- if ( ! result.success ) {
- throw new Error(
- result.data?.message || 'Failed to save settings'
- );
- }
-
- console.log( 'Successfully saved all onboarding settings' );
- } catch ( error ) {
- console.error( 'Failed to save settings:', error );
-
- // Display error message
- this.showErrorMessage(
- error.message || 'Failed to save settings. Please try again.',
- 'Error saving setting'
- );
-
- // Re-enable button
- this.setNextButtonDisabled( false );
-
- // Don't proceed to next step
- throw error;
- } finally {
- spinner.remove();
- }
- }
-}
-
-window.PrplSettingsStep = new PrplSettingsStep();
diff --git a/assets/js/onboarding/steps/WelcomeStep.js b/assets/js/onboarding/steps/WelcomeStep.js
deleted file mode 100644
index 8ad4498be3..0000000000
--- a/assets/js/onboarding/steps/WelcomeStep.js
+++ /dev/null
@@ -1,165 +0,0 @@
-/**
- * Welcome step - First step in the onboarding flow
- * Displays a welcome message, logo, and privacy policy checkbox
- */
-/* global OnboardingStep, LicenseGenerator, ProgressPlannerOnboardData */
-
-class PrplWelcomeStep extends OnboardingStep {
- constructor() {
- super( {
- templateId: 'onboarding-step-welcome',
- } );
- this.isGeneratingLicense = false;
- }
-
- /**
- * Mount the welcome step
- * Sets up checkbox listener and initializes state
- * @param {Object} state - Wizard state
- * @return {Function} Cleanup function
- */
- onMount( state ) {
- const checkbox = this.popover.querySelector( '#prpl-privacy-checkbox' );
-
- if ( ! checkbox ) {
- return () => {};
- }
-
- // Initialize state from checkbox if not already set in saved state
- if ( state.data.privacyAccepted === undefined ) {
- state.data.privacyAccepted = checkbox.checked;
- } else {
- // Set checkbox state from wizard state
- checkbox.checked = state.data.privacyAccepted;
- }
-
- const handler = ( e ) => {
- state.data.privacyAccepted = e.target.checked;
-
- // Remove active class from required indicator.
- this.popover
- .querySelector(
- '.prpl-privacy-checkbox-wrapper .prpl-required-indicator'
- )
- .classList.remove( 'prpl-required-indicator-active' );
- };
-
- checkbox.addEventListener( 'change', handler );
-
- return () => {
- checkbox.removeEventListener( 'change', handler );
- };
- }
-
- /**
- * Setup custom handler for disabled button clicks
- * Shows error message when user tries to proceed without accepting privacy policy
- * @return {Function} Cleanup function
- */
- onNextButtonSetup() {
- const disabledClickHandler = ( e ) => {
- if ( this.nextBtn.classList.contains( 'prpl-btn-disabled' ) ) {
- e.preventDefault();
- e.stopPropagation();
-
- this.popover
- .querySelector(
- '.prpl-privacy-checkbox-wrapper .prpl-required-indicator'
- )
- .classList.add( 'prpl-required-indicator-active' );
- }
- };
-
- this.nextBtn.addEventListener( 'click', disabledClickHandler );
-
- // Return cleanup function
- return () => {
- this.nextBtn?.removeEventListener( 'click', disabledClickHandler );
- };
- }
-
- /**
- * User can only proceed if privacy policy is accepted
- * Sites with existing license bypass this check (no privacy checkbox shown).
- * @param {Object} state - Wizard state
- * @return {boolean} True if privacy is accepted or license exists
- */
- canProceed( state ) {
- // Sites with license already skip the privacy checkbox.
- if ( ProgressPlannerOnboardData.hasLicense ) {
- return true;
- }
- return !! state.data.privacyAccepted;
- }
-
- /**
- * Called before advancing to next step
- * Generates license and shows spinner
- * Branded sites with existing license skip this step.
- * @return {Promise} Resolves when license is generated
- */
- async beforeNextStep() {
- // Skip license generation if site already has a license (branded sites).
- if ( ProgressPlannerOnboardData.hasLicense ) {
- return;
- }
-
- if ( this.isGeneratingLicense ) {
- return;
- }
-
- this.isGeneratingLicense = true;
-
- // Clear any existing error messages
- this.clearErrorMessage();
-
- // Show spinner
- const spinner = this.showSpinner( this.nextBtn );
-
- try {
- // Generate license
- await this.generateLicense();
- } catch ( error ) {
- console.error( 'Failed to generate license:', error );
-
- // Display error message to user
- this.showErrorMessage( error.message, 'Error generating license' );
-
- // Re-enable the button so user can retry
- this.setNextButtonDisabled( false );
-
- // Don't proceed to next step
- throw error;
- } finally {
- // Remove spinner
- spinner.remove();
- this.isGeneratingLicense = false;
- }
- }
-
- /**
- * Generate license on server
- * Uses LicenseGenerator utility class
- * @return {Promise} Resolves when license is generated
- */
- async generateLicense() {
- // Use LicenseGenerator to handle the license generation process
- return LicenseGenerator.generateLicense(
- {
- name: '',
- email: '',
- 'with-email': 'no',
- site: ProgressPlannerOnboardData.site,
- timezone_offset: ProgressPlannerOnboardData.timezone_offset,
- },
- {
- onboardNonceURL: ProgressPlannerOnboardData.onboardNonceURL,
- onboardAPIUrl: ProgressPlannerOnboardData.onboardAPIUrl,
- adminAjaxUrl: ProgressPlannerOnboardData.adminAjaxUrl,
- nonce: ProgressPlannerOnboardData.nonceProgressPlanner,
- }
- );
- }
-}
-
-window.PrplWelcomeStep = new PrplWelcomeStep();
diff --git a/assets/js/onboarding/steps/WhatsWhatStep.js b/assets/js/onboarding/steps/WhatsWhatStep.js
deleted file mode 100644
index c46a49c90a..0000000000
--- a/assets/js/onboarding/steps/WhatsWhatStep.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Whats What step - Explains the badge system to users
- * Simple informational step with no user interaction required
- */
-/* global OnboardingStep */
-class PrplWhatsWhatStep extends OnboardingStep {
- constructor() {
- super( {
- templateId: 'onboarding-step-whats-what',
- } );
- }
-
- /**
- * No special mounting logic needed for badges step
- * @param {Object} state - Wizard state
- * @return {Function} Cleanup function
- */
- onMount( state ) {
- // Whats Next step is informational only
- // No special logic needed
- return () => {};
- }
-
- /**
- * User can always proceed from badges step
- * @param {Object} state - Wizard state
- * @return {boolean} Always returns true
- */
- canProceed( state ) {
- return true;
- }
-}
-
-window.PrplWhatsWhatStep = new PrplWhatsWhatStep();
diff --git a/assets/js/settings.js b/assets/js/settings.js
index 32d7670391..6695710039 100644
--- a/assets/js/settings.js
+++ b/assets/js/settings.js
@@ -1,10 +1,10 @@
-/* global prplL10n, LicenseGenerator */
+/* global progressPlanner, progressPlannerAjaxRequest, progressPlannerSaveLicenseKey, prplL10n */
/*
* Settings
*
* A script to handle the settings page.
*
- * Dependencies: progress-planner/l10n, progress-planner/license-generator
+ * Dependencies: progress-planner/ajax-request, progress-planner/onboard, wp-util, progress-planner/l10n
*/
// Submit the email.
@@ -22,23 +22,47 @@ if ( !! settingsLicenseForm ) {
data[ key ] = value;
}
- document.getElementById( 'submit-license-key' ).disabled = true;
- document.getElementById( 'submit-license-key' ).innerHTML =
- prplL10n( 'subscribing' );
+ progressPlannerAjaxRequest( {
+ url: progressPlanner.onboardNonceURL,
+ data,
+ } )
+ .then( ( response ) => {
+ if ( 'ok' === response.status ) {
+ // Add the nonce to our data object.
+ data.nonce = response.nonce;
+
+ // Make the request to the API.
+ progressPlannerAjaxRequest( {
+ url: progressPlanner.onboardAPIUrl,
+ data,
+ } )
+ .then( ( apiResponse ) => {
+ // Make a local request to save the response data.
+ progressPlannerSaveLicenseKey(
+ apiResponse.license_key
+ );
- LicenseGenerator.generateLicense( data )
- .then( () => {
- document.getElementById( 'submit-license-key' ).innerHTML =
- prplL10n( 'subscribed' );
+ document.getElementById(
+ 'submit-license-key'
+ ).innerHTML = prplL10n( 'subscribed' );
- // Timeout so the license key is saved.
- setTimeout( () => {
- // Reload the page.
- window.location.reload();
- }, 500 );
+ // Timeout so the license key is saved.
+ setTimeout( () => {
+ // Reload the page.
+ window.location.reload();
+ }, 500 );
+ } )
+ .catch( ( error ) => {
+ console.warn( error );
+ } );
+ }
} )
.catch( ( error ) => {
console.warn( error );
} );
+
+ document.getElementById( 'submit-license-key' ).disabled = true;
+ document.getElementById( 'submit-license-key' ).innerHTML =
+ prplL10n( 'subscribing' );
} );
}
diff --git a/classes/admin/class-page-settings.php b/classes/admin/class-page-settings.php
index ba5e396897..e0976691cb 100644
--- a/classes/admin/class-page-settings.php
+++ b/classes/admin/class-page-settings.php
@@ -138,12 +138,13 @@ public function set_page_values( $pages ) {
}
/**
- * Save the settings.
+ * Save the redirect on login setting.
*
* @param bool $redirect_on_login Whether to redirect on login.
+ *
* @return void
*/
- public function save_settings( $redirect_on_login ) {
+ public function save_redirect_on_login( $redirect_on_login = false ) {
\update_user_meta( \get_current_user_id(), 'prpl_redirect_on_login', $redirect_on_login );
}
diff --git a/classes/admin/class-page.php b/classes/admin/class-page.php
index f5f36fc302..b4424fb24a 100644
--- a/classes/admin/class-page.php
+++ b/classes/admin/class-page.php
@@ -295,6 +295,15 @@ public function enqueue_styles() {
// Enqueue ugprading (onboarding) tasks styles, these are needed both when privacy policy is accepted and when it is not.
\progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/upgrade-tasks' );
}
+
+ $prpl_privacy_policy_accepted = \progress_planner()->is_privacy_policy_accepted();
+ if ( ! $prpl_privacy_policy_accepted ) {
+ // Enqueue welcome styles.
+ \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/welcome' );
+
+ // Enqueue onboarding styles.
+ \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/onboard' );
+ }
}
/**
diff --git a/classes/class-base.php b/classes/class-base.php
index 7978a9367d..7fa27f1ff3 100644
--- a/classes/class-base.php
+++ b/classes/class-base.php
@@ -55,7 +55,6 @@
* @method \Progress_Planner\Admin\Widgets\Challenge get_admin__widgets__challenge()
* @method \Progress_Planner\Admin\Widgets\Activity_Scores get_admin__widgets__activity_scores()
* @method \Progress_Planner\Utils\Date get_utils__date()
- * @method \Progress_Planner\Onboard_Wizard get_onboard_wizard()
*/
class Base {
@@ -181,9 +180,6 @@ public function init() {
// Init the enqueue class.
$this->get_admin__enqueue()->init();
-
- // TODO: Decide when this needs to be initialized.
- $this->get_onboard_wizard();
}
/**
diff --git a/classes/class-onboard-wizard.php b/classes/class-onboard-wizard.php
deleted file mode 100644
index f484b487e5..0000000000
--- a/classes/class-onboard-wizard.php
+++ /dev/null
@@ -1,604 +0,0 @@
-get_ui__branding()->get_branding_id();
- $show_onboarding = ! \progress_planner()->is_privacy_policy_accepted()
- || \get_option( self::PROGRESS_OPTION_NAME, false )
- || $is_branded;
-
- /**
- * Filter whether to show the onboarding wizard.
- *
- * Hosting integrations can use this filter to force showing
- * or hiding the onboarding wizard.
- *
- * @param bool $show_onboarding Whether to show the onboarding wizard.
- */
- $show_onboarding = \apply_filters( 'progress_planner_show_onboarding', $show_onboarding );
-
- if ( ! $show_onboarding ) {
- return;
- }
-
- // Add popover on front end.
- \add_action( 'wp_footer', [ $this, 'add_popover' ] );
- \add_action( 'wp_footer', [ $this, 'add_popover_step_templates' ] );
- \add_action( 'wp_enqueue_scripts', [ $this, 'add_popover_scripts' ] );
-
- // Add popover on admin.
- \add_action( 'admin_footer', [ $this, 'add_popover' ] );
- \add_action( 'admin_footer', [ $this, 'add_popover_step_templates' ] );
- \add_action( 'admin_enqueue_scripts', [ $this, 'add_popover_scripts' ] );
-
- // Trigger the onboarding wizard on the front end.
- \add_action( 'wp_footer', [ $this, 'trigger_onboarding' ] );
- \add_action( 'admin_footer', [ $this, 'trigger_onboarding' ] );
-
- // Define steps and their order.
- \add_action( 'init', [ $this, 'define_steps_and_order' ], 101 );
-
- // Allow only images for the front-end upload.
- \add_filter( 'rest_pre_insert_attachment', [ $this, 'rest_pre_insert_attachment' ], 10, 2 );
- }
-
- /**
- * Define steps and their order.
- *
- * @return void
- */
- public function define_steps_and_order() {
- $saved_progress = $this->get_saved_progress();
-
- // We need to know if the first task is already completed, in case user resumes the onboarding.
- $was_first_task_completed = isset( $saved_progress['data'] ) && ! empty( $saved_progress['data']['firstTaskCompleted'] );
-
- // Get the onboarding tasks.
- $onboarding_tasks = [
- 'core-blogdescription',
- 'select-timezone',
- 'select-locale',
- 'core-siteicon',
- ];
-
- $tasks = [];
-
- foreach ( $onboarding_tasks as $task_id ) {
- $task = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] );
- $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $task_id );
-
- // If there is no task, create it.
- if ( ! $task && $task_provider ) {
- $task_data = $task_provider->get_task_details();
-
- // Task will not be inserted if it already exists.
- \progress_planner()->get_suggested_tasks_db()->add( $task_data );
-
- // Now get the task.
- $task = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] );
- }
-
- // Safety check: Skip if task could not be created or retrieved.
- if ( empty( $task ) ) {
- \error_log( 'Onboarding: Could not retrieve or create task: ' . $task_id ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
- continue;
- }
-
- $task_formatted = [
- 'task_id' => $task[0]->get_task_id(),
- 'title' => $task[0]->post_title ?? '',
- 'url' => $task[0]->url ?? '',
- 'provider_id' => $task[0]->get_provider_id(),
- 'points' => $task[0]->points ?? 0,
- 'action_label' => $task_provider ? $task_provider->get_task_action_label() : \esc_html__( 'Do it', 'progress-planner' ),
- ];
-
- // Add task specific data.
- if ( 'core-blogdescription' === $task_id ) {
- $task_formatted['site_description'] = \get_bloginfo( 'description' );
- }
-
- $tasks[ $task_id ] = $task_formatted;
- }
-
- $this->steps = [
- [
- 'script_file_name' => 'WelcomeStep',
- 'template_file_name' => 'welcome',
- 'template_id' => 'onboarding-step-welcome',
- /* translators: %s: Progress Planner name. */
- 'title' => sprintf( esc_html__( 'Welcome to %s', 'progress-planner' ), \esc_html( \progress_planner()->get_ui__branding()->get_admin_menu_name() ) ),
- ],
- [
- 'script_file_name' => 'WhatsWhatStep',
- 'template_file_name' => 'whats-what',
- 'template_id' => 'onboarding-step-whats-what',
- 'title' => esc_html__( 'What\'s what?', 'progress-planner' ),
- ],
- ];
-
- // Add first task step if there are tasks or if the first task is already completed.
- if ( ! empty( $tasks ) || $was_first_task_completed ) {
- $this->steps[] = [
- 'script_file_name' => 'FirstTaskStep',
- 'template_file_name' => 'first-task',
- 'template_data' => ! $was_first_task_completed ? [ 'task' => \array_shift( $tasks ) ] : [],
- 'template_id' => 'onboarding-step-first-task',
- 'title' => esc_html__( 'Complete your first task!', 'progress-planner' ),
- ];
- }
-
- $this->steps[] = [
- 'script_file_name' => 'BadgesStep',
- 'template_file_name' => 'badges',
- 'template_id' => 'onboarding-step-badges',
- 'title' => esc_html__( 'Our badges are waiting for you', 'progress-planner' ),
- ];
-
- $this->steps[] = [
- 'script_file_name' => 'EmailFrequencyStep',
- 'template_file_name' => 'email-frequency',
- 'template_id' => 'onboarding-step-email-frequency',
- 'title' => esc_html__( 'Email Frequency', 'progress-planner' ),
- ];
-
- $this->steps[] = [
- 'script_file_name' => 'SettingsStep',
- 'template_file_name' => 'settings',
- 'template_id' => 'onboarding-step-settings',
- 'title' => esc_html__( 'Settings', 'progress-planner' ),
- ];
-
- // Add more-tasks step if there are remaining tasks.
- if ( ! empty( $tasks ) ) {
- $this->steps[] = [
- 'script_file_name' => 'MoreTasksStep',
- 'template_file_name' => 'more-tasks',
- 'template_data' => [ 'tasks' => $tasks ],
- 'template_id' => 'onboarding-step-more-tasks',
- 'title' => esc_html__( 'Finish onboarding!', 'progress-planner' ),
- ];
- }
- }
-
- /**
- * Allow only images for the front-end upload.
- *
- * @param array $attachment The attachment.
- * @param \WP_REST_Request $request The request.
- * @return array|\WP_Error The attachment or WP_Error.
- */
- public function rest_pre_insert_attachment( $attachment, $request ) {
-
- // Only run for our file upload.
- if ( isset( $request['prplFileUpload'] ) && $request['prplFileUpload'] ) {
-
- $files = $request->get_file_params();
-
- if ( empty( $files['file'] ) ) {
- return new \WP_Error(
- 'rest_no_file',
- __( 'No file uploaded.', 'progress-planner' ),
- [ 'status' => 400 ]
- );
- }
-
- $file = $files['file'];
-
- // Check MIME type.
- if ( strpos( $file['type'], 'image/' ) !== 0 ) {
- return new \WP_Error(
- 'rest_invalid_file_type',
- __( 'Only images are allowed for this upload.', 'progress-planner' ),
- [ 'status' => 400 ]
- );
- }
- }
-
- return $attachment;
- }
-
- /**
- * Add popover scripts.
- *
- * @return void
- */
- public function add_popover_scripts() {
- // Enqueue variables-color.css.
- \wp_enqueue_style( 'prpl-variables-color', \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/css/variables-color.css', [], \progress_planner()->get_plugin_version() );
-
- \wp_add_inline_style( 'prpl-variables-color', \progress_planner()->get_ui__branding()->get_custom_css() );
-
- // Enqueue onboarding.css.
- progress_planner()->get_admin__enqueue()->enqueue_style( 'onboarding/onboarding' );
-
- // Enqueue PrplOnboardTask (used by MoreTasksStep).
- \progress_planner()->get_admin__enqueue()->enqueue_script( 'onboarding/OnboardTask' );
-
- // Enqueue base step class.
- \progress_planner()->get_admin__enqueue()->enqueue_script( 'onboarding/steps/OnboardingStep' );
-
- // Enqueue step components.
- foreach ( $this->steps as $step ) {
- \progress_planner()->get_admin__enqueue()->enqueue_script( 'onboarding/steps/' . $step['script_file_name'] );
- }
-
- \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-gauge' );
-
- // Get saved progress from user meta.
- $saved_progress = $this->get_saved_progress();
-
- // Enqueue main onboarding.js (depends on all step components).
- \progress_planner()->get_admin__enqueue()->enqueue_script(
- 'onboarding/onboarding',
- [
- 'name' => 'ProgressPlannerOnboardData',
- 'data' => [
- 'adminAjaxUrl' => \esc_url_raw( admin_url( 'admin-ajax.php' ) ),
- 'nonceProgressPlanner' => \esc_js( \wp_create_nonce( 'progress_planner' ) ),
- 'nonceWPAPI' => \esc_js( \wp_create_nonce( 'wp_rest' ) ),
- 'popoverId' => 'prpl-popover-onboarding',
- 'onboardAPIUrl' => \progress_planner()->get_utils__onboard()->get_remote_url( 'onboard' ),
- 'onboardNonceURL' => \progress_planner()->get_utils__onboard()->get_remote_url( 'get-nonce' ),
- 'site' => \esc_attr( \set_url_scheme( \site_url() ) ),
- 'timezone_offset' => (float) ( \wp_timezone()->getOffset( new \DateTime( 'midnight' ) ) / 3600 ),
- 'savedProgress' => $saved_progress,
- 'lastStepRedirectUrl' => \esc_url_raw( admin_url( 'admin.php?page=progress-planner' ) ),
- 'fullscreenMode' => true, // Enable fullscreen takeover mode.
- 'hasLicense' => false !== \progress_planner()->get_license_key(),
- 'l10n' => [
- 'next' => \esc_html__( 'Next', 'progress-planner' ),
- 'startOnboarding' => \esc_html__( 'Start onboarding', 'progress-planner' ),
- /* translators: %s: Progress Planner name. */
- 'privacyPolicyError' => sprintf( \esc_html__( 'You need to agree with the privacy policy to use the %s plugin.', 'progress-planner' ), \esc_html( \progress_planner()->get_ui__branding()->get_admin_menu_name() ) ),
- /* translators: %s: arrow icon */
- 'dashboard' => sprintf( \esc_html__( 'Take me to the dashboard %s', 'progress-planner' ), '' ),
- 'backToRecommendations' => \esc_html__( 'Back to recommendations', 'progress-planner' ),
- ],
- 'errorIcon' => \progress_planner()->get_asset( 'images/icon_exclamation_circle.svg' ),
- 'steps' => array_column( $this->steps, 'script_file_name' ),
- ],
- ]
- );
- }
-
- /**
- * Get saved progress from user meta.
- *
- * @return array|null
- */
- protected function get_saved_progress() {
- if ( ! \get_current_user_id() ) {
- return null;
- }
-
- $onboarding_progress = \get_option( self::PROGRESS_OPTION_NAME, true );
- if ( ! $onboarding_progress ) {
- return null;
- }
-
- $decoded = \json_decode( $onboarding_progress, true );
- if ( ! $decoded || ! \is_array( $decoded ) ) {
- return null;
- }
-
- return $decoded;
- }
-
- /**
- * Verify AJAX security (capability and nonce).
- *
- * @return void
- */
- protected function verify_ajax_security() {
- if ( ! \current_user_can( 'manage_options' ) ) {
- \wp_send_json_error( [ 'message' => \esc_html__( 'You do not have permission to perform this action.', 'progress-planner' ) ] );
- }
-
- if ( ! \check_ajax_referer( 'progress_planner', 'nonce', false ) ) {
- \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] );
- }
- }
-
- /**
- * Save the tour progress.
- *
- * @return void
- */
- public function ajax_save_onboarding_progress() {
- $this->verify_ajax_security();
-
- if ( ! isset( $_POST['state'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in verify_ajax_security().
- \wp_send_json_error( [ 'message' => \esc_html__( 'State is required.', 'progress-planner' ) ] );
- }
- $progress = \sanitize_text_field( \wp_unslash( $_POST['state'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in verify_ajax_security().
-
- \error_log( print_r( $progress, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r, WordPress.PHP.DevelopmentFunctions.error_log_error_log
-
- // Save as user meta?
- \update_option( self::PROGRESS_OPTION_NAME, $progress );
-
- \wp_send_json_success( [ 'message' => \esc_html__( 'Tour progress saved.', 'progress-planner' ) ] );
- }
-
- /**
- * Complete a task.
- *
- * @return void
- */
- public function ajax_complete_task() {
- $this->verify_ajax_security();
-
- if ( ! isset( $_POST['task_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in verify_ajax_security().
- \wp_send_json_error( [ 'message' => \esc_html__( 'Task ID is required.', 'progress-planner' ) ] );
- }
-
- $task_id = \sanitize_text_field( \wp_unslash( $_POST['task_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in verify_ajax_security().
-
- // Aditional data for the task, besides the task ID.
- $form_values = [];
- if ( isset( $_POST['form_values'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in verify_ajax_security().
- $form_values = \sanitize_text_field( \wp_unslash( $_POST['form_values'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in verify_ajax_security().
- $form_values = \json_decode( $form_values, true );
- }
-
- // Safety check: Ensure form_values is an array after decoding.
- if ( ! \is_array( $form_values ) ) {
- $form_values = [];
- }
-
- // Get the task.
- $task = \progress_planner()->get_suggested_tasks_db()->get_post( $task_id );
- if ( ! $task ) {
- \wp_send_json_error( [ 'message' => \esc_html__( 'Task not found.', 'progress-planner' ) ] );
- }
-
- // To get the provider and complete the task, we need to use the provider.
- $provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $task->get_provider_id() );
- if ( ! $provider ) {
- \wp_send_json_error( [ 'message' => \esc_html__( 'Provider not found.', 'progress-planner' ) ] );
- }
-
- // Complete the task.
- $task_completed = $provider->complete_task( $form_values, $task_id );
-
- // It will skip the celebration and set the task's post status to trash.
- $task_post_marked_as_completed = \progress_planner()->get_suggested_tasks()->mark_task_as_completed( $task_id, null, true );
-
- if ( ! $task_completed || ! $task_post_marked_as_completed ) {
- \error_log( 'Task not completed: ' . $task_id ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
- \wp_send_json_error( [ 'message' => \esc_html__( 'Task not completed.', 'progress-planner' ) ] );
- }
-
- \error_log( 'Task completed: ' . $task_id ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
- \wp_send_json_success( [ 'message' => \esc_html__( 'Task completed.', 'progress-planner' ) ] );
- }
-
- /**
- * Handle saving all onboarding settings in a single request.
- *
- * @return void
- */
- public function ajax_save_all_onboarding_settings() {
- $this->verify_ajax_security();
-
- $page_settings = \progress_planner()->get_admin__page_settings();
-
- // Handle page settings (about, contact, faq).
- if ( isset( $_POST['pages'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
- $pages_json = \sanitize_text_field( \wp_unslash( $_POST['pages'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
- $pages = \json_decode( $pages_json, true );
-
- if ( \is_array( $pages ) ) {
- // Convert to the format expected by set_page_values.
- $pages_formatted = [];
- foreach ( $pages as $page_type => $page_data ) {
- if ( isset( $page_data['id'] ) && isset( $page_data['have_page'] ) ) {
- $pages_formatted[ $page_type ] = [
- 'id' => (int) $page_data['id'],
- 'have_page' => $page_data['have_page'],
- ];
- }
- }
-
- if ( ! empty( $pages_formatted ) ) {
- $page_settings->set_page_values( $pages_formatted );
- }
- }
- }
-
- // Handle post types.
- $include_post_types = isset( $_POST['prpl-post-types-include'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing
- ? \array_map( 'sanitize_text_field', \wp_unslash( $_POST['prpl-post-types-include'] ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
- : [];
- $page_settings->save_post_types( $include_post_types );
-
- // Handle login destination.
- $redirect_on_login = isset( $_POST['prpl-redirect-on-login'] ) ? (bool) \sanitize_text_field( \wp_unslash( $_POST['prpl-redirect-on-login'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing
- $page_settings->save_settings( $redirect_on_login );
-
- \wp_send_json_success( [ 'message' => \esc_html__( 'All settings saved successfully.', 'progress-planner' ) ] );
- }
-
- /**
- * Trigger the onboarding wizard on the front end.
- *
- * @return void
- */
- public function trigger_onboarding() {
-
- // If the request is an AJAX request, do not trigger the onboarding wizard.
- if ( \wp_doing_ajax() ) {
- return;
- }
-
- // Dont trigger it if user is not logged in and is not a admin.
- if ( ! \is_user_logged_in() || ! \current_user_can( 'manage_options' ) ) {
- return;
- }
-
- $get_saved_progress = $this->get_saved_progress();
-
- // If there is no saved progress, trigger the onboarding wizard.
- if ( ! $get_saved_progress ) {
- ?>
-
-
-
-
-
-
-
-
- steps[0]['title'] ); ?>
-
-
- steps as $step ) :
- ?>
- -
-
- [] ] ); ?>
-
-
-
-
-
- get_ui__branding()->the_logo(); ?>
-
-
-
-
-
-
-
-
-
- steps as $step ) {
- \progress_planner()->the_view( 'onboarding/' . $step['template_file_name'] . '.php', isset( $step['template_data'] ) ? $step['template_data'] : [] );
- }
-
- // Add quit confirmation template.
- \progress_planner()->the_view( 'onboarding/quit-confirmation.php' );
- ?>
-
- mark_task_as_completed( $task_id, $user_id );
- }
-
- /**
- * Complete a task.
- *
- * @param string $task_id The task ID.
- * @param int|null $user_id Optional. The user ID for token deletion. If provided, the token will be deleted.
- * @param bool $skip_celebration Optional. Whether to skip the celebration.
- *
- * @return bool
- */
- public function mark_task_as_completed( $task_id, $user_id = null, $skip_celebration = false ) {
if ( ! $this->was_task_completed( $task_id ) ) {
$task = \progress_planner()->get_suggested_tasks_db()->get_post( $task_id );
if ( $task ) {
- $post_status = $skip_celebration ? 'trash' : 'pending';
- \progress_planner()->get_suggested_tasks_db()->update_recommendation( $task->ID, [ 'post_status' => $post_status ] );
+ \progress_planner()->get_suggested_tasks_db()->update_recommendation( $task->ID, [ 'post_status' => 'pending' ] );
// Insert an activity.
$this->insert_activity( $task_id );
- // Delete the token after successful use (one-time use) if user_id is provided.
- if ( $user_id ) {
- $this->delete_task_completion_token( $task_id, $user_id );
- }
-
- return true;
+ // Delete the token after successful use (one-time use).
+ $this->delete_task_completion_token( $task_id, $user_id );
}
}
-
- return false;
}
/**
diff --git a/classes/suggested-tasks/class-tasks-interface.php b/classes/suggested-tasks/class-tasks-interface.php
index bf9a9ca598..18b3f8f3aa 100644
--- a/classes/suggested-tasks/class-tasks-interface.php
+++ b/classes/suggested-tasks/class-tasks-interface.php
@@ -114,13 +114,6 @@ public function get_popover_id();
*/
public function add_task_actions( $data = [], $actions = [] );
- /**
- * Get the task action label.
- *
- * @return string
- */
- public function get_task_action_label();
-
/**
* Check if the task has activity.
*
@@ -129,14 +122,4 @@ public function get_task_action_label();
* @return bool
*/
public function task_has_activity( $task_id = '' );
-
- /**
- * Complete the task.
- *
- * @param array $args The task data.
- * @param string $task_id The task ID.
- *
- * @return bool
- */
- public function complete_task( $args = [], $task_id = '' );
}
diff --git a/classes/suggested-tasks/providers/class-blog-description.php b/classes/suggested-tasks/providers/class-blog-description.php
index add79362a3..61ec8bb519 100644
--- a/classes/suggested-tasks/providers/class-blog-description.php
+++ b/classes/suggested-tasks/providers/class-blog-description.php
@@ -142,42 +142,9 @@ public function print_popover_form_contents() {
public function add_task_actions( $data = [], $actions = [] ) {
$actions[] = [
'priority' => 10,
- 'html' => '' . \esc_html( $this->get_task_action_label() ) . '',
+ 'html' => '' . \esc_html__( 'Set tagline', 'progress-planner' ) . '',
];
return $actions;
}
-
- /**
- * Get the task action label.
- *
- * @return string
- */
- public function get_task_action_label() {
- return \__( 'Set tagline', 'progress-planner' );
- }
-
- /**
- * Complete the task.
- *
- * @param array $args The task data.
- * @param string $task_id The task ID.
- *
- * @return bool
- */
- public function complete_task( $args = [], $task_id = '' ) {
-
- if ( ! $this->capability_required() ) {
- return false;
- }
-
- if ( ! isset( $args['blogdescription'] ) ) {
- return false;
- }
-
- // update_option will return false if the option value is the same as the one being set.
- \update_option( 'blogdescription', \sanitize_text_field( $args['blogdescription'] ) );
-
- return true;
- }
}
diff --git a/classes/suggested-tasks/providers/class-select-locale.php b/classes/suggested-tasks/providers/class-select-locale.php
index b15251b393..313d4aa5cf 100644
--- a/classes/suggested-tasks/providers/class-select-locale.php
+++ b/classes/suggested-tasks/providers/class-select-locale.php
@@ -269,13 +269,27 @@ public function handle_interactive_task_specific_submit() {
\wp_send_json_error( [ 'message' => \esc_html__( 'Missing setting path.', 'progress-planner' ) ] );
}
+ $option_updated = false;
$language_for_update = \sanitize_text_field( \wp_unslash( $_POST['value'] ) );
if ( empty( $language_for_update ) ) {
\wp_send_json_error( [ 'message' => \esc_html__( 'Invalid language.', 'progress-planner' ) ] );
}
- $option_updated = $this->update_language( $language_for_update );
+ // Handle translation installation.
+ if ( \current_user_can( 'install_languages' ) ) {
+ // @phpstan-ignore-next-line requireOnce.fileNotFound
+ require_once ABSPATH . 'wp-admin/includes/translation-install.php';
+
+ if ( \wp_can_install_language_pack() ) {
+ $language = \wp_download_language_pack( $language_for_update );
+ if ( $language ) {
+ $language_for_update = $language;
+
+ $option_updated = \update_option( 'WPLANG', $language_for_update );
+ }
+ }
+ }
if ( $option_updated ) {
\wp_send_json_success( [ 'message' => \esc_html__( 'Setting updated.', 'progress-planner' ) ] );
@@ -295,68 +309,9 @@ public function handle_interactive_task_specific_submit() {
public function add_task_actions( $data = [], $actions = [] ) {
$actions[] = [
'priority' => 10,
- 'html' => '' . \esc_html( $this->get_task_action_label() ) . '',
+ 'html' => '' . \esc_html__( 'Select locale', 'progress-planner' ) . '',
];
return $actions;
}
-
- /**
- * Get the task action label.
- *
- * @return string
- */
- public function get_task_action_label() {
- return \__( 'Select locale', 'progress-planner' );
- }
-
- /**
- * Complete the task.
- *
- * @param array $args The task data.
- * @param string $task_id The task ID.
- *
- * @return bool
- */
- public function complete_task( $args = [], $task_id = '' ) {
-
- if ( ! $this->capability_required() ) {
- return false;
- }
-
- if ( ! isset( $args['language'] ) ) {
- return false;
- }
-
- return $this->update_language( \sanitize_text_field( \wp_unslash( $args['language'] ) ) );
- }
-
- /**
- * Update the language.
- *
- * @param string $language_for_update The language to update.
- *
- * @return bool
- */
- protected function update_language( $language_for_update ) {
- // Handle translation installation.
- if ( \current_user_can( 'install_languages' ) ) {
- // @phpstan-ignore-next-line requireOnce.fileNotFound
- require_once ABSPATH . 'wp-admin/includes/translation-install.php';
-
- if ( \wp_can_install_language_pack() ) {
- $language = \wp_download_language_pack( $language_for_update );
- if ( $language ) {
- $language_for_update = $language;
-
- // update_option will return false if the option value is the same as the one being set.
- \update_option( 'WPLANG', $language_for_update );
-
- return true;
- }
- }
- }
-
- return false;
- }
}
diff --git a/classes/suggested-tasks/providers/class-select-timezone.php b/classes/suggested-tasks/providers/class-select-timezone.php
index 14807d3bfe..3eb54c8bf1 100644
--- a/classes/suggested-tasks/providers/class-select-timezone.php
+++ b/classes/suggested-tasks/providers/class-select-timezone.php
@@ -192,75 +192,6 @@ public function handle_interactive_task_specific_submit() {
\wp_send_json_error( [ 'message' => \esc_html__( 'Invalid timezone.', 'progress-planner' ) ] );
}
- $option_updated = $this->update_timezone( $timezone_string );
-
- if ( $option_updated ) {
-
- // We're not checking for the return value of the update_option calls, because it will return false if the value is the same (for example if gmt_offset is already set to '').
- \wp_send_json_success( [ 'message' => \esc_html__( 'Setting updated.', 'progress-planner' ) ] );
- }
-
- \wp_send_json_error( [ 'message' => \esc_html__( 'Failed to update setting.', 'progress-planner' ) ] );
- }
-
- /**
- * Add task actions specific to this task.
- *
- * @param array $data The task data.
- * @param array $actions The existing actions.
- *
- * @return array
- */
- public function add_task_actions( $data = [], $actions = [] ) {
- $actions[] = [
- 'priority' => 10,
- 'html' => '' . \esc_html( $this->get_task_action_label() ) . '',
- ];
-
- return $actions;
- }
-
- /**
- * Get the task action label.
- *
- * @return string
- */
- public function get_task_action_label() {
- return \__( 'Select timezone', 'progress-planner' );
- }
-
- /**
- * Complete the task.
- *
- * @param array $args The task data.
- * @param string $task_id The task ID.
- *
- * @return bool
- */
- public function complete_task( $args = [], $task_id = '' ) {
-
- if ( ! $this->capability_required() ) {
- return false;
- }
-
- if ( ! isset( $args['timezone'] ) ) {
- return false;
- }
-
- $timezone_string = \sanitize_text_field( \wp_unslash( $args['timezone'] ) );
-
- return $this->update_timezone( $timezone_string );
- }
-
- /**
- * Update the timezone.
- *
- * @param string $timezone_string The timezone string to update.
- *
- * @return bool
- */
- protected function update_timezone( $timezone_string ) {
-
$update_options = false;
// Map UTC+- timezones to gmt_offsets and set timezone_string to empty.
@@ -283,9 +214,27 @@ protected function update_timezone( $timezone_string ) {
\update_option( 'timezone_string', $timezone_string );
\update_option( 'gmt_offset', $gmt_offset );
- return true;
+ // We're not checking for the return value of the update_option calls, because it will return false if the value is the same (for example if gmt_offset is already set to '').
+ \wp_send_json_success( [ 'message' => \esc_html__( 'Setting updated.', 'progress-planner' ) ] );
}
- return false;
+ \wp_send_json_error( [ 'message' => \esc_html__( 'Failed to update setting.', 'progress-planner' ) ] );
+ }
+
+ /**
+ * Add task actions specific to this task.
+ *
+ * @param array $data The task data.
+ * @param array $actions The existing actions.
+ *
+ * @return array
+ */
+ public function add_task_actions( $data = [], $actions = [] ) {
+ $actions[] = [
+ 'priority' => 10,
+ 'html' => '' . \esc_html__( 'Select timezone', 'progress-planner' ) . '',
+ ];
+
+ return $actions;
}
}
diff --git a/classes/suggested-tasks/providers/class-site-icon.php b/classes/suggested-tasks/providers/class-site-icon.php
index 7dfd56f88d..7bf34c85f4 100644
--- a/classes/suggested-tasks/providers/class-site-icon.php
+++ b/classes/suggested-tasks/providers/class-site-icon.php
@@ -154,42 +154,9 @@ protected function get_enqueue_data() {
public function add_task_actions( $data = [], $actions = [] ) {
$actions[] = [
'priority' => 10,
- 'html' => '' . \esc_html( $this->get_task_action_label() ) . '',
+ 'html' => '' . \esc_html__( 'Set site icon', 'progress-planner' ) . '',
];
return $actions;
}
-
- /**
- * Get the task action label.
- *
- * @return string
- */
- public function get_task_action_label() {
- return \__( 'Set site icon', 'progress-planner' );
- }
-
- /**
- * Complete the task.
- *
- * @param array $args The task data.
- * @param string $task_id The task ID.
- *
- * @return bool
- */
- public function complete_task( $args = [], $task_id = '' ) {
-
- if ( ! $this->capability_required() ) {
- return false;
- }
-
- if ( ! isset( $args['post_id'] ) ) {
- return false;
- }
-
- // update_option will return false if the option value is the same as the one being set.
- \update_option( 'site_icon', \sanitize_text_field( $args['post_id'] ) );
-
- return true;
- }
}
diff --git a/classes/suggested-tasks/providers/class-tasks.php b/classes/suggested-tasks/providers/class-tasks.php
index d074ed83a9..854bfebe99 100644
--- a/classes/suggested-tasks/providers/class-tasks.php
+++ b/classes/suggested-tasks/providers/class-tasks.php
@@ -268,7 +268,7 @@ public function get_external_link_url() {
* Get the task ID.
*
* Generates a unique task ID by combining the provider ID with optional task-specific data.
- * For repetitive tasks, includes the current year-week (YW format) to create weekly instances.
+ * For repetitive tasks, includes the current year-week (oW format) to create weekly instances.
*
* Example task IDs:
* - Non-repetitive: "update-core"
@@ -782,15 +782,6 @@ public function add_task_actions( $data = [], $actions = [] ) {
return $actions;
}
- /**
- * Get the task action label.
- *
- * @return string
- */
- public function get_task_action_label() {
- return \__( 'Do it', 'progress-planner' );
- }
-
/**
* Check if the task has activity.
*
@@ -812,16 +803,4 @@ public function task_has_activity( $task_id = '' ) {
return ! empty( $activity );
}
-
- /**
- * Complete the task.
- *
- * @param array $args The task data.
- * @param string $task_id The task ID.
- *
- * @return bool
- */
- public function complete_task( $args = [], $task_id = '' ) {
- return false;
- }
}
diff --git a/classes/utils/class-debug-tools.php b/classes/utils/class-debug-tools.php
index 6b42a5ca69..d6f1d5e302 100644
--- a/classes/utils/class-debug-tools.php
+++ b/classes/utils/class-debug-tools.php
@@ -48,7 +48,6 @@ public function __construct() {
\add_action( 'init', [ $this, 'check_toggle_migrations' ] );
\add_action( 'init', [ $this, 'check_delete_single_task' ] );
\add_action( 'init', [ $this, 'check_toggle_recommendations_ui' ] );
- \add_action( 'init', [ $this, 'check_delete_onboarding_progress' ] );
if ( \defined( '\IS_PLAYGROUND_PREVIEW' ) && \constant( '\IS_PLAYGROUND_PREVIEW' ) === true ) {
\add_action( 'init', [ $this, 'check_toggle_placeholder_demo' ] );
}
@@ -100,8 +99,6 @@ public function add_toolbar_items( $admin_bar ) {
$this->add_toggle_recommendations_ui_submenu_item( $admin_bar );
$this->add_placeholder_demo_submenu_item( $admin_bar );
-
- $this->add_onboarding_submenu_item( $admin_bar );
}
/**
@@ -730,74 +727,4 @@ public function filter_tasks_show_ui( $show_ui ) {
}
return $show_ui;
}
-
- /**
- * Add Onboarding submenu to the debug menu.
- *
- * @param \WP_Admin_Bar $admin_bar The WordPress admin bar object.
- * @return void
- */
- protected function add_onboarding_submenu_item( $admin_bar ) {
- $admin_bar->add_node(
- [
- 'id' => 'prpl-onboarding',
- 'parent' => 'prpl-debug',
- 'title' => 'Onboarding',
- ]
- );
-
- // Start onboarding.
- $admin_bar->add_node(
- [
- 'id' => 'prpl-start-onboarding',
- 'parent' => 'prpl-onboarding',
- 'title' => 'Start Onboarding',
- 'href' => '#',
- 'meta' => [
- 'onclick' => 'window.prplOnboardWizard.startOnboarding(); return false;',
- ],
- ]
- );
-
- // Delete onboarding progress.
- $admin_bar->add_node(
- [
- 'id' => 'prpl-delete-onboarding-progress',
- 'parent' => 'prpl-onboarding',
- 'title' => 'Delete Onboarding Progress',
- 'href' => \add_query_arg( 'prpl_delete_onboarding_progress', '1', $this->current_url ),
- ]
- );
- }
-
- /**
- * Check and process the delete onboarding progress action.
- *
- * Deletes onboarding progress if the appropriate query parameter is set
- * and user has required capabilities.
- *
- * @return void
- */
- public function check_delete_onboarding_progress() {
- if (
- ! isset( $_GET['prpl_delete_onboarding_progress'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended
- $_GET['prpl_delete_onboarding_progress'] !== '1' || // phpcs:ignore WordPress.Security.NonceVerification.Recommended
- ! \current_user_can( 'manage_options' )
- ) {
- return;
- }
-
- // Verify nonce for security.
- $this->verify_nonce();
-
- // Delete the onboarding progress.
- \Progress_Planner\Onboard_Wizard::delete_progress();
-
- // Delete the license key.
- \delete_option( 'progress_planner_license_key' );
-
- // Redirect to the same page without the parameter.
- \wp_safe_redirect( \remove_query_arg( [ 'prpl_delete_onboarding_progress', '_wpnonce' ] ) );
- exit;
- }
}
diff --git a/classes/utils/class-playground.php b/classes/utils/class-playground.php
index 130a004315..988043db81 100644
--- a/classes/utils/class-playground.php
+++ b/classes/utils/class-playground.php
@@ -31,6 +31,7 @@ public function register_hooks() {
if ( ! \progress_planner()->get_license_key() && ! \get_option( 'progress_planner_demo_data_generated', false ) ) {
$this->generate_data();
\update_option( 'progress_planner_license_key', \str_replace( ' ', '-', $this->create_random_string( 20 ) ) );
+ \update_option( 'progress_planner_force_show_onboarding', false );
\update_option(
'progress_planner_todo',
[
@@ -47,6 +48,7 @@ public function register_hooks() {
\update_option( 'progress_planner_demo_data_generated', true );
}
\add_action( 'progress_planner_admin_page_header_before', [ $this, 'show_header_notice' ] );
+ \add_action( 'wp_ajax_progress_planner_hide_onboarding', [ $this, 'hide_onboarding' ] );
\add_action( 'wp_ajax_progress_planner_show_onboarding', [ $this, 'show_onboarding' ] );
\progress_planner()->get_settings()->set( 'activation_date', ( new \DateTime() )->modify( '-2 months' )->format( 'Y-m-d' ) );
@@ -78,23 +80,48 @@ public function enable_debug_tools() {
}
/**
- * Show the onboarding in the Playground environment.
+ * Toggle the onboarding visibility in the Playground environment.
+ *
+ * @param string $action Either 'show' or 'hide'.
*
* @return void
*/
- public function show_onboarding() {
- \check_ajax_referer( 'progress_planner_show_onboarding', 'nonce' );
+ private function toggle_onboarding( $action ) {
+ $nonce_action = "progress_planner_{$action}_onboarding";
+ \check_ajax_referer( $nonce_action, 'nonce' );
if ( ! \current_user_can( 'manage_options' ) ) {
\wp_die( \esc_html__( 'You do not have sufficient permissions to access this page.', 'progress-planner' ) );
}
- // Delete onboarding progress to trigger fresh onboarding.
- \Progress_Planner\Onboard_Wizard::delete_progress();
- // Delete the license key to trigger onboarding (privacy not accepted).
- \delete_option( 'progress_planner_license_key' );
+ if ( $action === 'hide' ) {
+ \add_option( 'progress_planner_license_key', \str_replace( ' ', '-', $this->create_random_string( 20 ) ) );
+ $message = \esc_html__( 'Onboarding hidden successfully', 'progress-planner' );
+ } else {
+ \delete_option( 'progress_planner_license_key' );
+ $message = \esc_html__( 'Onboarding shown successfully', 'progress-planner' );
+ }
+ \update_option( 'progress_planner_force_show_onboarding', $action !== 'hide' );
+
+ \wp_send_json_success( [ 'message' => $message ] );
+ }
+
+ /**
+ * Hide the onboarding in the Playground environment.
+ *
+ * @return void
+ */
+ public function hide_onboarding() {
+ $this->toggle_onboarding( 'hide' );
+ }
- \wp_send_json_success( [ 'message' => \esc_html__( 'Onboarding shown successfully', 'progress-planner' ) ] );
+ /**
+ * Show the onboarding in the Playground environment.
+ *
+ * @return void
+ */
+ public function show_onboarding() {
+ $this->toggle_onboarding( 'show' );
}
/**
@@ -108,7 +135,10 @@ public function show_header_notice() {
return;
}
- $nonce = \wp_create_nonce( 'progress_planner_show_onboarding' );
+ $show_onboarding = \get_option( 'progress_planner_force_show_onboarding', false );
+ $button_text = $show_onboarding ? \__( 'Hide onboarding', 'progress-planner' ) : \__( 'Show onboarding', 'progress-planner' );
+ $action = $show_onboarding ? 'hide' : 'show';
+ $nonce = \wp_create_nonce( "progress_planner_{$action}_onboarding" );
?>