From 653f4a1c9b373230c5927ab7727be8e4e94c7940 Mon Sep 17 00:00:00 2001 From: David Stone Date: Sat, 14 Feb 2026 23:30:18 -0700 Subject: [PATCH 01/10] Fix password strength not enforced on checkout (minStrength hardcoded) checkout.js hardcoded minStrength: 3 when creating the password checker, overriding the PHP-configured minimum strength setting. This meant the "strong" (score 4) and "super_strong" settings were effectively treated as "medium" (score 3), allowing weaker passwords through. Removed the hardcoded override so the checker reads min_strength from wu_password_strength_settings (set by PHP). Also added re-initialization in the Vue updated() hook so the password checker activates when the field appears after initial mount in multi-step checkouts. Co-Authored-By: Claude Opus 4.6 --- assets/js/checkout.js | 6 +- .../fixtures/set-password-strength.php | 32 ++++ .../050-password-strength-enforcement.spec.js | 171 ++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/cypress/fixtures/set-password-strength.php create mode 100644 tests/e2e/cypress/integration/050-password-strength-enforcement.spec.js diff --git a/assets/js/checkout.js b/assets/js/checkout.js index 1c2fa7ec..6569ab73 100644 --- a/assets/js/checkout.js +++ b/assets/js/checkout.js @@ -795,7 +795,6 @@ this.password_strength_checker = new window.WU_PasswordStrength({ pass1: pass1_el, result: jQuery('#pass-strength-result'), - minStrength: 3, onValidityChange(isValid) { that.valid_password = isValid; @@ -1078,6 +1077,11 @@ // Setup inline login handlers if prompt is visible this.setup_inline_login_handlers(); + // Re-initialize password strength if field appeared after mount + if (! this.password_strength_checker && jQuery('#field-password').length) { + this.init_password_strength(); + } + }); }, diff --git a/tests/e2e/cypress/fixtures/set-password-strength.php b/tests/e2e/cypress/fixtures/set-password-strength.php new file mode 100644 index 00000000..232a0480 --- /dev/null +++ b/tests/e2e/cypress/fixtures/set-password-strength.php @@ -0,0 +1,32 @@ + + * Where is one of: medium, strong, super_strong + * + * Outputs the new setting value as confirmation. + */ + +$args = $GLOBALS['argv'] ?? []; + +// The strength value is passed as a positional argument after '--'. +$strength = end($args); + +$valid = ['medium', 'strong', 'super_strong']; + +if (! in_array($strength, $valid, true)) { + echo wp_json_encode([ + 'error' => 'Invalid strength value', + 'value' => $strength, + 'allowed' => $valid, + ]); + exit(1); +} + +wu_save_setting('minimum_password_strength', $strength); + +echo wp_json_encode([ + 'success' => true, + 'setting' => wu_get_setting('minimum_password_strength'), +]); diff --git a/tests/e2e/cypress/integration/050-password-strength-enforcement.spec.js b/tests/e2e/cypress/integration/050-password-strength-enforcement.spec.js new file mode 100644 index 00000000..1fb38cba --- /dev/null +++ b/tests/e2e/cypress/integration/050-password-strength-enforcement.spec.js @@ -0,0 +1,171 @@ +describe("Password Strength Enforcement", () => { + /** + * Helper: set the password strength setting via WP-CLI fixture. + */ + function setPasswordStrength(level) { + const containerPath = + "/var/www/html/wp-content/plugins/ultimate-multisite/tests/e2e/cypress/fixtures/set-password-strength.php"; + cy.exec( + `npx wp-env run tests-cli wp eval-file ${containerPath} -- ${level}`, + { timeout: 60000 } + ).then((result) => { + const data = JSON.parse(result.stdout.trim()); + expect(data.setting).to.equal(level); + }); + } + + /** + * Helper: visit the register page with a fresh browser state. + */ + function visitRegisterPage() { + cy.clearCookies(); + cy.visit("/register", { failOnStatusCode: false }); + + // Wait for checkout form to render + cy.get("#field-password", { timeout: 30000 }).should("be.visible"); + cy.wait(2000); + } + + /** + * Helper: type a password and return validation state from Vue. + */ + function typePasswordAndGetState(password) { + cy.get("#field-password").clear().type(password); + cy.wait(500); // Allow strength checker to process + + return cy.window().then((win) => { + const vue = win.document.querySelector("#wu_form").__vue__; + const score = win.wp.passwordStrength.meter(password, [], ""); + const label = win.document.querySelector( + "#pass-strength-result" + ).textContent; + + return { + valid_password: vue.valid_password, + zxcvbn_score: score, + strength_label: label, + minStrength: vue.password_strength_checker + ? vue.password_strength_checker.options.minStrength + : null, + }; + }); + } + + // ───────────────────────────────────────────────── + // Test: Strong setting (default) + // ───────────────────────────────────────────────── + describe("Strong setting (zxcvbn score >= 4)", () => { + before(() => { + setPasswordStrength("strong"); + }); + + beforeEach(() => { + visitRegisterPage(); + }); + + it("should reject a medium-strength password (score 3)", () => { + typePasswordAndGetState("Summer2025!xyz").then((state) => { + expect(state.zxcvbn_score).to.equal(3); + expect(state.valid_password).to.equal(false); + expect(state.minStrength).to.equal(4); + expect(state.strength_label).to.equal("Medium"); + }); + }); + + it("should accept a strong password (score 4)", () => { + typePasswordAndGetState("correct horse battery").then((state) => { + expect(state.zxcvbn_score).to.equal(4); + expect(state.valid_password).to.equal(true); + expect(state.strength_label).to.equal("Strong"); + }); + }); + + it("should reject a weak password (score 2)", () => { + typePasswordAndGetState("Butterfly923!").then((state) => { + expect(state.zxcvbn_score).to.be.lessThan(3); + expect(state.valid_password).to.equal(false); + }); + }); + }); + + // ───────────────────────────────────────────────── + // Test: Super Strong setting (score >= 4 + char rules) + // ───────────────────────────────────────────────── + describe("Super Strong setting (score >= 4 + character rules)", () => { + before(() => { + setPasswordStrength("super_strong"); + }); + + beforeEach(() => { + visitRegisterPage(); + }); + + it("should reject a score-3 password even with all character types", () => { + typePasswordAndGetState("Summer2025!xyz").then((state) => { + expect(state.zxcvbn_score).to.equal(3); + expect(state.valid_password).to.equal(false); + // Should show Medium, not Super Strong + expect(state.strength_label).to.not.contain("Super Strong"); + }); + }); + + it("should reject a score-4 password missing character types", () => { + // score 4 but no uppercase, numbers, or special chars + typePasswordAndGetState("correct horse battery").then((state) => { + expect(state.zxcvbn_score).to.equal(4); + expect(state.valid_password).to.equal(false); + expect(state.strength_label).to.contain("Required:"); + }); + }); + + it("should reject a password shorter than 12 characters", () => { + // Strong score but too short for super_strong + typePasswordAndGetState("xK9#mL2$vN").then((state) => { + expect(state.valid_password).to.equal(false); + }); + }); + + it("should accept a password with score 4 and all character types", () => { + typePasswordAndGetState("xK9#mL2$vN5@qR").then((state) => { + expect(state.zxcvbn_score).to.equal(4); + expect(state.valid_password).to.equal(true); + expect(state.strength_label).to.equal("Super Strong"); + }); + }); + }); + + // ───────────────────────────────────────────────── + // Test: Medium setting (score >= 3) + // ───────────────────────────────────────────────── + describe("Medium setting (zxcvbn score >= 3)", () => { + before(() => { + setPasswordStrength("medium"); + }); + + beforeEach(() => { + visitRegisterPage(); + }); + + it("should accept a medium-strength password (score 3)", () => { + typePasswordAndGetState("Summer2025!xyz").then((state) => { + expect(state.zxcvbn_score).to.equal(3); + expect(state.valid_password).to.equal(true); + expect(state.minStrength).to.equal(3); + }); + }); + + it("should reject a weak password (score 2)", () => { + typePasswordAndGetState("Butterfly923!").then((state) => { + expect(state.zxcvbn_score).to.be.lessThan(3); + expect(state.valid_password).to.equal(false); + }); + }); + }); + + // ───────────────────────────────────────────────── + // Cleanup: restore to strong (default) + // ───────────────────────────────────────────────── + after(() => { + setPasswordStrength("strong"); + }); +}); From 862fe7331f5758667b9bf6cbd43cd166f782fd16 Mon Sep 17 00:00:00 2001 From: David Stone Date: Sun, 15 Feb 2026 17:16:31 -0700 Subject: [PATCH 02/10] Security audit fixes and e2e test improvements Address critical, high, and medium security issues found in code audit since v2.4.10: - Add capability check and timing-safe state comparison to Stripe OAuth callback - Require authentication and nonce for payment status polling AJAX - Replace base64 fallback with sodium encryption in credential store - Add pattern-based filtering for sensitive settings in REST API - Add PWYW maximum amount validation and input sanitization on Cart - Restore double-replacement guard in site duplication - Keep billing_country required on free trials for tax calculation - Revert sandbox mode default to true for Stripe gateways - Add GitHub domain validation for beta update package URLs - Add re-entry guard to prevent recursive HTTP filter Fix e2e tests: - Handle custom login page redirect in loginByForm command - Fix billing field visibility with v-show defaulting to visible during AJAX - Reset password strength setting in setup to prevent cross-run failures - Use stronger test passwords that pass all strength levels Co-Authored-By: Claude Opus 4.6 --- assets/css/checkout.min.css | 5 +- assets/js/checkout.min.js | 2 +- assets/js/gateways/stripe.min.js | 10 +- inc/apis/class-settings-endpoint.php | 26 +- inc/checkout/class-cart.php | 79 +- inc/checkout/class-checkout-pages.php | 1 + inc/checkout/class-checkout.php | 4 +- .../class-signup-field-billing-address.php | 2 +- inc/class-wp-ultimo.php | 25 +- inc/duplication/data.php | 6 + inc/gateways/class-base-stripe-gateway.php | 18 +- inc/gateways/class-stripe-gateway.php | 2 +- inc/helpers/class-credential-store.php | 102 +- inc/managers/class-gateway-manager.php | 4 +- inc/stuff.php | 8 +- lang/ultimate-multisite.pot | 1224 ++++++++++------- .../e2e/cypress/integration/000-setup.spec.js | 8 + .../010-manual-checkout-flow.spec.js | 24 +- .../integration/020-free-trial-flow.spec.js | 2 +- .../030-stripe-checkout-flow.spec.js | 2 +- tests/e2e/cypress/support/commands/login.js | 8 +- 21 files changed, 1006 insertions(+), 556 deletions(-) diff --git a/assets/css/checkout.min.css b/assets/css/checkout.min.css index dbd7d2f2..b3a3278b 100644 --- a/assets/css/checkout.min.css +++ b/assets/css/checkout.min.css @@ -1,5 +1,4 @@ -.wu-payment-status{padding:12px 16px;border-radius:6px;margin-bottom:16px;font-weight:500}.wu-payment-status-checking,.wu-payment-status-pending{background-color:#fef3cd;color:#856404;border:1px solid #ffc107}.wu-payment-status-completed{background-color:#d4edda;color:#155724;border:1px solid #28a745}.wu-payment-status-error,.wu-payment-status-timeout{background-color:#f8d7da;color:#721c24;border:1px solid #f5c6cb} -:root{--wu-accent-fallback-wp:var( +.wu-payment-status{padding:12px 16px;border-radius:6px;margin-bottom:16px;font-weight:500}.wu-payment-status-checking,.wu-payment-status-pending{background-color:#fef3cd;color:#856404;border:1px solid #ffc107}.wu-payment-status-completed{background-color:#d4edda;color:#155724;border:1px solid #28a745}.wu-payment-status-error,.wu-payment-status-timeout{background-color:#f8d7da;color:#721c24;border:1px solid #f5c6cb}:root{--wu-accent-fallback-wp:var( --wp--preset--color--accent, var( --wp--preset--color--primary, @@ -11,4 +10,4 @@ --e-global-color-primary, var(--wu-accent-fallback-kadence) ) - );--wu-input-bg:#fff;--wu-input-color:#1e1e1e;--wu-input-border-color:#949494;--wu-input-border-radius:4px;--wu-input-padding:8px 12px;--wu-input-font-size:16px;--wu-input-line-height:1.5;--wu-input-focus-border-color:var(--wu-accent-color);--wu-input-focus-shadow:0 0 0 1px var(--wu-accent-color);--wu-label-font-size:14px;--wu-label-font-weight:600;--wu-label-color:inherit;--wu-submit-bg:var(--wu-accent-color);--wu-submit-color:#fff;--wu-submit-padding:10px 24px;--wu-submit-border-radius:4px;--wu-submit-font-size:15px;--wu-addon-bg:rgba(0, 0, 0, 0.03);--wu-addon-border-color:var(--wu-input-border-color);--wu-addon-font-size:90%}.wu-styling .form-control{width:100%;box-sizing:border-box;padding:var(--wu-input-padding);font-size:var(--wu-input-font-size);line-height:var(--wu-input-line-height);color:var(--wu-input-color);background-color:var(--wu-input-bg);border:1px solid var(--wu-input-border-color);border-radius:var(--wu-input-border-radius);appearance:none;-webkit-appearance:none;transition:border-color .15s ease,box-shadow .15s ease;max-width:100%}.wu-styling .form-control:focus{border-color:var(--wu-input-focus-border-color);box-shadow:var(--wu-input-focus-shadow);outline:0}.wu-styling .form-control::placeholder{color:#949494;opacity:1}.wu-styling select.form-control{padding-right:36px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23555' d='M1.41 0L6 4.58 10.59 0 12 1.41l-6 6-6-6z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;background-size:12px 8px}.wu-styling textarea.form-control{min-height:80px;resize:vertical}.wu-styling label.wu-block{display:block;font-size:var(--wu-label-font-size);font-weight:var(--wu-label-font-weight);color:var(--wu-label-color);margin-bottom:4px}.wu-styling .wu-password-field-container{display:block;width:100%}.wu-styling .wu-input-addon{display:flex;align-items:center;justify-content:center;padding:0 12px;box-sizing:border-box;font-family:monospace;font-size:var(--wu-addon-font-size);background-color:var(--wu-addon-bg);border:1px solid var(--wu-addon-border-color);white-space:nowrap}.wu-styling .wu-input-addon-prefix{margin-right:-1px;border-top-left-radius:var(--wu-input-border-radius);border-bottom-left-radius:var(--wu-input-border-radius);border-top-right-radius:0;border-bottom-right-radius:0}.wu-styling .wu-input-addon-suffix{margin-left:-1px;border-top-right-radius:var(--wu-input-border-radius);border-bottom-right-radius:var(--wu-input-border-radius);border-top-left-radius:0;border-bottom-left-radius:0}.wu-styling .wu-input-grouped{border-radius:0}.wu-styling button.button-primary,.wu-styling input[type=submit].button-primary{display:inline-block;box-sizing:border-box;padding:var(--wu-submit-padding);font-size:var(--wu-submit-font-size);font-weight:600;line-height:1.5;color:var(--wu-submit-color);background-color:var(--wu-submit-bg);border:1px solid transparent;border-radius:var(--wu-submit-border-radius);cursor:pointer;text-align:center;text-decoration:none;appearance:none;-webkit-appearance:none;transition:filter .15s ease}.wu-styling button.button-primary:hover,.wu-styling input[type=submit].button-primary:hover{filter:brightness(.9)}.wu-styling button.button-primary:focus,.wu-styling input[type=submit].button-primary:focus{outline:2px solid var(--wu-accent-color);outline-offset:2px} + );--wu-input-bg:#fff;--wu-input-color:#1e1e1e;--wu-input-border-color:#949494;--wu-input-border-radius:4px;--wu-input-padding:8px 12px;--wu-input-font-size:16px;--wu-input-line-height:1.5;--wu-input-focus-border-color:var(--wu-accent-color);--wu-input-focus-shadow:0 0 0 1px var(--wu-accent-color);--wu-label-font-size:14px;--wu-label-font-weight:600;--wu-label-color:inherit;--wu-submit-bg:var(--wu-accent-color);--wu-submit-color:#fff;--wu-submit-padding:10px 24px;--wu-submit-border-radius:4px;--wu-submit-font-size:15px;--wu-addon-bg:rgba(0, 0, 0, 0.03);--wu-addon-border-color:var(--wu-input-border-color);--wu-addon-font-size:90%}.wu-styling .form-control{width:100%;box-sizing:border-box;padding:var(--wu-input-padding);font-size:var(--wu-input-font-size);line-height:var(--wu-input-line-height);color:var(--wu-input-color);background-color:var(--wu-input-bg);border:1px solid var(--wu-input-border-color);border-radius:var(--wu-input-border-radius);appearance:none;-webkit-appearance:none;transition:border-color .15s ease,box-shadow .15s ease;max-width:100%}.wu-styling .form-control:focus{border-color:var(--wu-input-focus-border-color);box-shadow:var(--wu-input-focus-shadow);outline:0}.wu-styling .form-control::placeholder{color:#949494;opacity:1}.wu-styling select.form-control{padding-right:36px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23555' d='M1.41 0L6 4.58 10.59 0 12 1.41l-6 6-6-6z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;background-size:12px 8px}.wu-styling textarea.form-control{min-height:80px;resize:vertical}.wu-styling label.wu-block{display:block;font-size:var(--wu-label-font-size);font-weight:var(--wu-label-font-weight);color:var(--wu-label-color);margin-bottom:4px}.wu-styling .wu-password-field-container{display:block;width:100%}.wu-styling .wu-input-addon{display:flex;align-items:center;justify-content:center;padding:0 12px;box-sizing:border-box;font-family:monospace;font-size:var(--wu-addon-font-size);background-color:var(--wu-addon-bg);border:1px solid var(--wu-addon-border-color);white-space:nowrap}.wu-styling .wu-input-addon-prefix{margin-right:-1px;border-top-left-radius:var(--wu-input-border-radius);border-bottom-left-radius:var(--wu-input-border-radius);border-top-right-radius:0;border-bottom-right-radius:0}.wu-styling .wu-input-addon-suffix{margin-left:-1px;border-top-right-radius:var(--wu-input-border-radius);border-bottom-right-radius:var(--wu-input-border-radius);border-top-left-radius:0;border-bottom-left-radius:0}.wu-styling .wu-input-grouped{border-radius:0}.wu-styling button.button-primary,.wu-styling input[type=submit].button-primary{display:inline-block;box-sizing:border-box;padding:var(--wu-submit-padding);font-size:var(--wu-submit-font-size);font-weight:600;line-height:1.5;color:var(--wu-submit-color);background-color:var(--wu-submit-bg);border:1px solid transparent;border-radius:var(--wu-submit-border-radius);cursor:pointer;text-align:center;text-decoration:none;appearance:none;-webkit-appearance:none;transition:filter .15s ease}.wu-styling button.button-primary:hover,.wu-styling input[type=submit].button-primary:hover{filter:brightness(.9)}.wu-styling button.button-primary:focus,.wu-styling input[type=submit].button-primary:focus{outline:2px solid var(--wu-accent-color);outline-offset:2px} \ No newline at end of file diff --git a/assets/js/checkout.min.js b/assets/js/checkout.min.js index 16e23a55..aab73c4e 100644 --- a/assets/js/checkout.min.js +++ b/assets/js/checkout.min.js @@ -1 +1 @@ -((r,n,s)=>{window.history.replaceState&&window.history.replaceState(null,null,wu_checkout.baseurl),n.addAction("wu_on_create_order","nextpress/wp-ultimo",function(e,t){void 0!==t.order.extra.template_id&&t.order.extra.template_id&&(e.template_id=t.order.extra.template_id)}),n.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(e){void 0!==window.wu_auto_submittable_field&&window.wu_auto_submittable_field&&e.$watch(window.wu_auto_submittable_field,function(){jQuery(this.$el).submit()},{deep:!0})}),n.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(t){wu_create_cookie("wu_template",""),wu_create_cookie("wu_selected_products",""),wu_listen_to_cookie_change("wu_template",function(e){e&&(t.template_id=e)})}),r(document).on("click",'[href|="#wu-checkout-add"]',function(e){e.preventDefault();var e=r(this),t=e.attr("href").split("#").pop().replace("wu-checkout-add-","");"undefined"!=typeof wu_checkout_form&&-1===wu_checkout_form.products.indexOf(t)&&(wu_checkout_form.add_product(t),e.html(wu_checkout.i18n.added_to_order))}),window.addEventListener("pageshow",function(e){e.persisted&&this.window.wu_checkout_form&&this.window.wu_checkout_form.unblock()}),r(document).ready(function(){var e;void 0!==window.Vue&&(Object.defineProperty(Vue.prototype,"$moment",{value:moment}),e={plan:(e=function(e){return isNaN(e)?e:parseInt(e,10)})(wu_checkout.plan),errors:[],order:wu_checkout.order,products:s.map(wu_checkout.products,e),template_id:wu_checkout.template_id,template_category:"",gateway:wu_checkout.gateway,request_billing_address:wu_checkout.request_billing_address,country:wu_checkout.country,state:"",city:"",site_title:wu_checkout.site_title||"",site_url:wu_checkout.site_url,site_domain:wu_checkout.site_domain,is_subdomain:wu_checkout.is_subdomain,discount_code:wu_checkout.discount_code,toggle_discount_code:0,payment_method:"",username:"",email_address:"",payment_id:wu_checkout.payment_id,membership_id:wu_checkout.membership_id,cart_type:"new",auto_renew:1,duration:wu_checkout.duration,duration_unit:wu_checkout.duration_unit,prevent_submission:!1,valid_password:!0,stored_templates:{},state_list:[],city_list:[],labels:{},show_login_prompt:!1,login_prompt_field:"",checking_user_exists:!1,logging_in:!1,login_error:"",inline_login_password:"",custom_amounts:wu_checkout.custom_amounts||{},pwyw_recurring:wu_checkout.pwyw_recurring||{}},n.applyFilters("wu_before_form_init",e),jQuery("#wu_form").length)&&(Vue.component("colorPicker",{props:["value"],template:'',mounted(){let o=this;r(this.$el).val(this.value).wpColorPicker({width:200,defaultColor:this.value,change(e,t){o.$emit("input",t.color.toString())}})},watch:{value(e){r(this.$el).wpColorPicker("color",e)}},destroyed(){r(this.$el).off().wpColorPicker("destroy")}}),window.wu_checkout_form=new Vue({el:"#wu_form",data:e,directives:{init:{bind(e,t,o){o.context[t.arg]=t.value}}},components:{dynamic:{functional:!0,template:"#dynamic",props:["template"],render(e,t){t=t.props.template;return e(t?{template:t}:"
nbsp;
")}}},computed:{hooks(){return wp.hooks},unique_products(){return s.uniq(this.products,!1,e=>parseInt(e,10))}},methods:{debounce(e){return s.debounce(e,200,!0)},open_url(e,t="_blank"){window.open(e,t)},get_template(e,t){void 0===t.id&&(t.id="default");var o=e+"/"+t.id;return void 0!==this.stored_templates[o]?this.stored_templates[o]:(o=this.hooks.applyFilters("wu_before_template_fetch",{duration:this.duration,duration_unit:this.duration_unit,products:this.products,...t},this),this.fetch_template(e,o),'
'+wu_checkout.i18n.loading+"
")},reset_templates(r){if(void 0===r)this.stored_templates={};else{let i={};s.forEach(this.stored_templates,function(e,t){var o=t.toString().substr(0,t.toString().indexOf("/"));!1===s.contains(r,o)&&(i[t]=e)}),this.stored_templates=i}},fetch_template(o,i){let r=this;void 0===i.id&&(i.id="default"),this.request("wu_render_field_template",{template:o,attributes:i},function(e){var t=o+"/"+i.id;e.success?Vue.set(r.stored_templates,t,e.data.html):Vue.set(r.stored_templates,t,"
"+e.data[0].message+"
")})},go_back(){this.block(),window.history.back()},set_prevent_submission(e){this.$nextTick(function(){this.prevent_submission=e})},remove_product(t,o){this.products=s.filter(this.products,function(e){return e!=t&&e!=o})},add_plan(e){this.plan&&this.remove_product(this.plan),this.plan=e,this.add_product(e)},add_product(e){this.products.push(e)},has_product(e){return-1',overlayCSS:{backgroundColor:e||"#ffffff",opacity:.6},css:{padding:0,margin:0,width:"50%",fontSize:"14px !important",top:"40%",left:"35%",textAlign:"center",color:"#000",border:"none",backgroundColor:"none",cursor:"wait"}})},unblock(){jQuery(this.$el).wu_unblock()},request(e,t,o,i){var r="wu_validate_form"===e||"wu_create_order"===e||"wu_render_field_template"===e||"wu_check_user_exists"===e||"wu_inline_login"===e?wu_checkout.late_ajaxurl:wu_checkout.ajaxurl;jQuery.ajax({method:"POST",url:r+"&action="+e,data:t,success:o,error:i})},init_password_strength(){let t=this;var e=jQuery("#field-password");e.length&&void 0!==window.WU_PasswordStrength&&(this.valid_password=!1,this.password_strength_checker=new window.WU_PasswordStrength({pass1:e,result:jQuery("#pass-strength-result"),minStrength:3,onValidityChange(e){t.valid_password=e}}))},check_user_exists_debounced:s.debounce(function(e,t){this.check_user_exists(e,t)},500),check_user_exists(o,e){if(!e||e.length<3)this.show_login_prompt=!1;else{this.checking_user_exists=!0,this.login_error="";let t=this;this.request("wu_check_user_exists",{field_type:o,value:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.checking_user_exists=!1,e.success&&e.data.exists?(t.show_login_prompt=!0,t.login_prompt_field=o):t.show_login_prompt=!1},function(e){t.checking_user_exists=!1,t.show_login_prompt=!1})}},handle_inline_login(e){if(e&&(e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation()),this.inline_login_password){this.logging_in=!0,this.login_error="";let t=this;e="email"===this.login_prompt_field?this.email_address||"":this.username||"";this.request("wu_inline_login",{username_or_email:e,password:this.inline_login_password,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.logging_in=!1,e.success&&window.location.reload()},function(e){t.logging_in=!1,e.responseJSON&&e.responseJSON.data&&e.responseJSON.data.message?t.login_error=e.responseJSON.data.message:t.login_error=wu_checkout.i18n.login_failed||"Login failed. Please try again."})}else this.login_error=wu_checkout.i18n.password_required||"Password is required";return!1},dismiss_login_prompt(){this.show_login_prompt=!1,this.inline_login_password="",this.login_error=""},setup_inline_login_handlers(){let d=this;["email","username"].forEach(function(n){var e=document.getElementById("wu-inline-login-password-"+n),t=document.getElementById("wu-inline-login-submit-"+n);if(e&&t){var s=document.getElementById("wu-dismiss-login-prompt-"+n);let o=document.getElementById("wu-login-error-"+n);var a=document.getElementById("wu-inline-login-prompt-"+n);let i=t.cloneNode(!0),r=(t.parentNode.replaceChild(i,t),e.cloneNode(!0));function u(e){i.disabled=!1,i.textContent=wu_checkout.i18n.sign_in||"Sign in",e.data&&e.data.message?o.textContent=e.data.message:o.textContent=wu_checkout.i18n.login_failed||"Login failed. Please try again.",o.style.display="block"}function _(e){e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation();e=r.value;if(!e)return o.textContent=wu_checkout.i18n.password_required||"Password is required",!(o.style.display="block");i.disabled=!0,i.innerHTML=''+(wu_checkout.i18n.logging_in||"Logging in..."),o.style.display="none";var t="email"===n?d.email_address:d.username;return jQuery.ajax({method:"POST",url:wu_checkout.late_ajaxurl+"&action=wu_inline_login",data:{username_or_email:t,password:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},success(e){e.success?window.location.reload():u(e)},error:u}),!1}e.parentNode.replaceChild(r,e),a&&(a.addEventListener("click",function(e){e.stopPropagation()}),a.addEventListener("keydown",function(e){e.stopPropagation()}),a.addEventListener("keyup",function(e){e.stopPropagation()})),i.addEventListener("click",_),r.addEventListener("keydown",function(e){"Enter"===e.key&&_(e)}),s&&s.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),d.show_login_prompt=!1,d.inline_login_password="",r.value=""})}})}},updated(){this.$nextTick(function(){n.doAction("wu_on_form_updated",this),wu_initialize_tooltip(),this.setup_inline_login_handlers()})},mounted(){let i=this;jQuery(this.$el).on("click",function(e){r(this).data("submited_via",r(e.target))}),jQuery(this.$el).on("submit",async function(e){e.preventDefault();var t,e=jQuery(this).data("submited_via");e&&((t=jQuery("")).attr("type","hidden"),t.attr("name",e.attr("name")),t.attr("value",e.val()),jQuery(this).append(t)),i.block();try{var o=[];await Promise.all(n.applyFilters("wu_before_form_submitted",o,i,i.gateway))}catch(e){return i.errors=[],i.errors.push({code:"before-submit-error",message:e.message}),i.unblock(),void i.handle_errors(e)}i.validate_form(),n.doAction("wu_on_form_submitted",i,i.gateway)}),this.create_order(),n.doAction("wu_checkout_loaded",this),n.doAction("wu_on_change_gateway",this.gateway,this.gateway),this.init_password_strength(),wu_initialize_tooltip()},watch:{products(e,t){this.on_change_product(e,t)},toggle_discount_code(e){e||(this.discount_code="")},discount_code(e,t){this.on_change_discount_code(e,t)},gateway(e,t){this.on_change_gateway(e,t)},country(e,t){this.state="",this.on_change_country(e,t)},state(e,t){this.city="",this.on_change_state(e,t)},city(e,t){this.on_change_city(e,t)},duration(e,t){this.on_change_duration(e,t)},duration_unit(e,t){this.on_change_duration_unit(e,t)}}}))})})(jQuery,wp.hooks,_); \ No newline at end of file +((r,n,s)=>{window.history.replaceState&&window.history.replaceState(null,null,wu_checkout.baseurl),n.addAction("wu_on_create_order","nextpress/wp-ultimo",function(e,t){void 0!==t.order.extra.template_id&&t.order.extra.template_id&&(e.template_id=t.order.extra.template_id)}),n.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(e){void 0!==window.wu_auto_submittable_field&&window.wu_auto_submittable_field&&e.$watch(window.wu_auto_submittable_field,function(){jQuery(this.$el).submit()},{deep:!0})}),n.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(t){wu_create_cookie("wu_template",""),wu_create_cookie("wu_selected_products",""),wu_listen_to_cookie_change("wu_template",function(e){e&&(t.template_id=e)})}),r(document).on("click",'[href|="#wu-checkout-add"]',function(e){e.preventDefault();var e=r(this),t=e.attr("href").split("#").pop().replace("wu-checkout-add-","");"undefined"!=typeof wu_checkout_form&&-1===wu_checkout_form.products.indexOf(t)&&(wu_checkout_form.add_product(t),e.html(wu_checkout.i18n.added_to_order))}),window.addEventListener("pageshow",function(e){e.persisted&&this.window.wu_checkout_form&&this.window.wu_checkout_form.unblock()}),r(document).ready(function(){var e;void 0!==window.Vue&&(Object.defineProperty(Vue.prototype,"$moment",{value:moment}),e={plan:(e=function(e){return isNaN(e)?e:parseInt(e,10)})(wu_checkout.plan),errors:[],order:wu_checkout.order,products:s.map(wu_checkout.products,e),template_id:wu_checkout.template_id,template_category:"",gateway:wu_checkout.gateway,request_billing_address:wu_checkout.request_billing_address,country:wu_checkout.country,state:"",city:"",site_title:wu_checkout.site_title||"",site_url:wu_checkout.site_url,site_domain:wu_checkout.site_domain,is_subdomain:wu_checkout.is_subdomain,discount_code:wu_checkout.discount_code,toggle_discount_code:0,payment_method:"",username:"",email_address:"",payment_id:wu_checkout.payment_id,membership_id:wu_checkout.membership_id,cart_type:"new",auto_renew:1,duration:wu_checkout.duration,duration_unit:wu_checkout.duration_unit,prevent_submission:!1,valid_password:!0,stored_templates:{},state_list:[],city_list:[],labels:{},show_login_prompt:!1,login_prompt_field:"",checking_user_exists:!1,logging_in:!1,login_error:"",inline_login_password:"",custom_amounts:wu_checkout.custom_amounts||{},pwyw_recurring:wu_checkout.pwyw_recurring||{}},n.applyFilters("wu_before_form_init",e),jQuery("#wu_form").length)&&(Vue.component("colorPicker",{props:["value"],template:'',mounted(){let o=this;r(this.$el).val(this.value).wpColorPicker({width:200,defaultColor:this.value,change(e,t){o.$emit("input",t.color.toString())}})},watch:{value(e){r(this.$el).wpColorPicker("color",e)}},destroyed(){r(this.$el).off().wpColorPicker("destroy")}}),window.wu_checkout_form=new Vue({el:"#wu_form",data:e,directives:{init:{bind(e,t,o){o.context[t.arg]=t.value}}},components:{dynamic:{functional:!0,template:"#dynamic",props:["template"],render(e,t){t=t.props.template;return e(t?{template:t}:"
nbsp;
")}}},computed:{hooks(){return wp.hooks},unique_products(){return s.uniq(this.products,!1,e=>parseInt(e,10))}},methods:{debounce(e){return s.debounce(e,200,!0)},open_url(e,t="_blank"){window.open(e,t)},get_template(e,t){void 0===t.id&&(t.id="default");var o=e+"/"+t.id;return void 0!==this.stored_templates[o]?this.stored_templates[o]:(o=this.hooks.applyFilters("wu_before_template_fetch",{duration:this.duration,duration_unit:this.duration_unit,products:this.products,...t},this),this.fetch_template(e,o),'
'+wu_checkout.i18n.loading+"
")},reset_templates(r){if(void 0===r)this.stored_templates={};else{let i={};s.forEach(this.stored_templates,function(e,t){var o=t.toString().substr(0,t.toString().indexOf("/"));!1===s.contains(r,o)&&(i[t]=e)}),this.stored_templates=i}},fetch_template(o,i){let r=this;void 0===i.id&&(i.id="default"),this.request("wu_render_field_template",{template:o,attributes:i},function(e){var t=o+"/"+i.id;e.success?Vue.set(r.stored_templates,t,e.data.html):Vue.set(r.stored_templates,t,"
"+e.data[0].message+"
")})},go_back(){this.block(),window.history.back()},set_prevent_submission(e){this.$nextTick(function(){this.prevent_submission=e})},remove_product(t,o){this.products=s.filter(this.products,function(e){return e!=t&&e!=o})},add_plan(e){this.plan&&this.remove_product(this.plan),this.plan=e,this.add_product(e)},add_product(e){this.products.push(e)},has_product(e){return-1',overlayCSS:{backgroundColor:e||"#ffffff",opacity:.6},css:{padding:0,margin:0,width:"50%",fontSize:"14px !important",top:"40%",left:"35%",textAlign:"center",color:"#000",border:"none",backgroundColor:"none",cursor:"wait"}})},unblock(){jQuery(this.$el).wu_unblock()},request(e,t,o,i){var r="wu_validate_form"===e||"wu_create_order"===e||"wu_render_field_template"===e||"wu_check_user_exists"===e||"wu_inline_login"===e?wu_checkout.late_ajaxurl:wu_checkout.ajaxurl;jQuery.ajax({method:"POST",url:r+"&action="+e,data:t,success:o,error:i})},init_password_strength(){let t=this;var e=jQuery("#field-password");e.length&&void 0!==window.WU_PasswordStrength&&(this.valid_password=!1,this.password_strength_checker=new window.WU_PasswordStrength({pass1:e,result:jQuery("#pass-strength-result"),onValidityChange(e){t.valid_password=e}}))},check_user_exists_debounced:s.debounce(function(e,t){this.check_user_exists(e,t)},500),check_user_exists(o,e){if(!e||e.length<3)this.show_login_prompt=!1;else{this.checking_user_exists=!0,this.login_error="";let t=this;this.request("wu_check_user_exists",{field_type:o,value:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.checking_user_exists=!1,e.success&&e.data.exists?(t.show_login_prompt=!0,t.login_prompt_field=o):t.show_login_prompt=!1},function(e){t.checking_user_exists=!1,t.show_login_prompt=!1})}},handle_inline_login(e){if(e&&(e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation()),this.inline_login_password){this.logging_in=!0,this.login_error="";let t=this;e="email"===this.login_prompt_field?this.email_address||"":this.username||"";this.request("wu_inline_login",{username_or_email:e,password:this.inline_login_password,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.logging_in=!1,e.success&&window.location.reload()},function(e){t.logging_in=!1,e.responseJSON&&e.responseJSON.data&&e.responseJSON.data.message?t.login_error=e.responseJSON.data.message:t.login_error=wu_checkout.i18n.login_failed||"Login failed. Please try again."})}else this.login_error=wu_checkout.i18n.password_required||"Password is required";return!1},dismiss_login_prompt(){this.show_login_prompt=!1,this.inline_login_password="",this.login_error=""},setup_inline_login_handlers(){let d=this;["email","username"].forEach(function(n){var e=document.getElementById("wu-inline-login-password-"+n),t=document.getElementById("wu-inline-login-submit-"+n);if(e&&t){var s=document.getElementById("wu-dismiss-login-prompt-"+n);let o=document.getElementById("wu-login-error-"+n);var a=document.getElementById("wu-inline-login-prompt-"+n);let i=t.cloneNode(!0),r=(t.parentNode.replaceChild(i,t),e.cloneNode(!0));function u(e){i.disabled=!1,i.textContent=wu_checkout.i18n.sign_in||"Sign in",e.data&&e.data.message?o.textContent=e.data.message:o.textContent=wu_checkout.i18n.login_failed||"Login failed. Please try again.",o.style.display="block"}function _(e){e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation();e=r.value;if(!e)return o.textContent=wu_checkout.i18n.password_required||"Password is required",!(o.style.display="block");i.disabled=!0,i.innerHTML=''+(wu_checkout.i18n.logging_in||"Logging in..."),o.style.display="none";var t="email"===n?d.email_address:d.username;return jQuery.ajax({method:"POST",url:wu_checkout.late_ajaxurl+"&action=wu_inline_login",data:{username_or_email:t,password:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},success(e){e.success?window.location.reload():u(e)},error:u}),!1}e.parentNode.replaceChild(r,e),a&&(a.addEventListener("click",function(e){e.stopPropagation()}),a.addEventListener("keydown",function(e){e.stopPropagation()}),a.addEventListener("keyup",function(e){e.stopPropagation()})),i.addEventListener("click",_),r.addEventListener("keydown",function(e){"Enter"===e.key&&_(e)}),s&&s.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),d.show_login_prompt=!1,d.inline_login_password="",r.value=""})}})}},updated(){this.$nextTick(function(){n.doAction("wu_on_form_updated",this),wu_initialize_tooltip(),this.setup_inline_login_handlers(),!this.password_strength_checker&&jQuery("#field-password").length&&this.init_password_strength()})},mounted(){let i=this;jQuery(this.$el).on("click",function(e){r(this).data("submited_via",r(e.target))}),jQuery(this.$el).on("submit",async function(e){e.preventDefault();var t,e=jQuery(this).data("submited_via");e&&((t=jQuery("")).attr("type","hidden"),t.attr("name",e.attr("name")),t.attr("value",e.val()),jQuery(this).append(t)),i.block();try{var o=[];await Promise.all(n.applyFilters("wu_before_form_submitted",o,i,i.gateway))}catch(e){return i.errors=[],i.errors.push({code:"before-submit-error",message:e.message}),i.unblock(),void i.handle_errors(e)}i.validate_form(),n.doAction("wu_on_form_submitted",i,i.gateway)}),this.create_order(),n.doAction("wu_checkout_loaded",this),n.doAction("wu_on_change_gateway",this.gateway,this.gateway),this.init_password_strength(),wu_initialize_tooltip()},watch:{products(e,t){this.on_change_product(e,t)},toggle_discount_code(e){e||(this.discount_code="")},discount_code(e,t){this.on_change_discount_code(e,t)},gateway(e,t){this.on_change_gateway(e,t)},country(e,t){this.state="",this.on_change_country(e,t)},state(e,t){this.city="",this.on_change_state(e,t)},city(e,t){this.on_change_city(e,t)},duration(e,t){this.on_change_duration(e,t)},duration_unit(e,t){this.on_change_duration_unit(e,t)}}}))})})(jQuery,wp.hooks,_); \ No newline at end of file diff --git a/assets/js/gateways/stripe.min.js b/assets/js/gateways/stripe.min.js index 7a2c28dd..1ce0f6c2 100644 --- a/assets/js/gateways/stripe.min.js +++ b/assets/js/gateways/stripe.min.js @@ -1,3 +1,7 @@ -let _stripe,stripeElement,card,stripeElements=function(e){e=(_stripe=Stripe(e)).elements();card=e.create("card",{hidePostalCode:!0}),wp.hooks.addFilter("wu_before_form_submitted","nextpress/wp-ultimo",function(e,t,r){var o=document.getElementById("card-element");return"stripe"===r&&0{try{var r=await _stripe.createPaymentMethod({type:"card",card:card});r.error&&t(r.error)}catch(e){}e()})),e}),wp.hooks.addAction("wu_on_form_success","nextpress/wp-ultimo",function(e,t){"stripe"===e.gateway&&(0{try{var n=(await elements.submit()).error;n?t(n):e()}catch(e){t(e)}})),e}),wp.hooks.addAction("wu_on_form_success","nextpress/wp-ultimo",function(e,t){var n,r;"stripe"!==e.gateway||e.order.totals.total<=0&&e.order.totals.recurring.total<=0||(t.gateway.data.stripe_client_secret?(n=t.gateway.data.stripe_client_secret,r=t.gateway.data.stripe_intent_type,e.set_prevent_submission(!1),r="payment_intent"===r?"confirmPayment":"confirmSetup",(t={elements:elements,confirmParams:{return_url:window.location.href,payment_method_data:{billing_details:{name:t.customer.display_name,email:t.customer.user_email}}},redirect:"if_required"}).clientSecret=n,_stripe[r](t).then(function(e){e.error?(wu_checkout_form.unblock(),wu_checkout_form.errors.push(e.error)):wu_checkout_form.resubmit()})):e.set_prevent_submission(!1))}),wp.hooks.addAction("wu_on_form_updated","nextpress/wp-ultimo",function(t){if("stripe"!==t.gateway){if(t.set_prevent_submission(!1),paymentElement){try{paymentElement.unmount()}catch(e){}paymentElement=null,elements=null,currentElementsMode=null,currentElementsAmount=null}}else if(document.getElementById("payment-element")){var e=t.order?t.order.totals.total:0,n=0currency = $this->attributes->currency; $this->duration = $this->attributes->duration; $this->duration_unit = $this->attributes->duration_unit; - $this->custom_amounts = is_array($this->attributes->custom_amounts) ? $this->attributes->custom_amounts : []; - $this->pwyw_recurring = is_array($this->attributes->pwyw_recurring) ? $this->attributes->pwyw_recurring : []; + $this->custom_amounts = self::sanitize_pwyw_amounts(is_array($this->attributes->custom_amounts) ? $this->attributes->custom_amounts : []); + $this->pwyw_recurring = self::sanitize_pwyw_recurring(is_array($this->attributes->pwyw_recurring) ? $this->attributes->pwyw_recurring : []); /* * Loads the current customer, if it exists. @@ -1773,6 +1773,29 @@ public function add_product($product_id_or_slug, $quantity = 1): bool { return false; } + /** + * Maximum allowed amount for PWYW products. + * + * @since 2.4.11 + * @param float $max_amount The maximum allowed amount. + * @param \WP_Ultimo\Models\Product $product The product. + */ + $max_amount = (float) apply_filters('wu_pwyw_maximum_amount', 50000.00, $product); + + if ($custom_amount > $max_amount) { + $this->errors->add( + 'pwyw-above-maximum', + sprintf( + // translators: %1$s is the product name, %2$s is the maximum amount formatted as currency + __('The amount for %1$s cannot exceed %2$s.', 'ultimate-multisite'), + $product->get_name(), + wu_format_currency($max_amount, $product->get_currency()) + ) + ); + + return false; + } + $amount = (float) $custom_amount; } else { // Use suggested amount as default @@ -2992,4 +3015,56 @@ public function get_pwyw_recurring_for_product($product_id): bool { return (bool) wu_get_isset($this->pwyw_recurring, $product_id, false); } + + /** + * Sanitize PWYW custom amounts array from user input. + * + * Ensures all keys are integers (product IDs) and all values are non-negative floats. + * + * @since 2.4.11 + * @param array $amounts Raw amounts array from request. + * @return array Sanitized amounts. + */ + private static function sanitize_pwyw_amounts(array $amounts): array { + + $clean = []; + + foreach ($amounts as $key => $value) { + $product_id = (int) $key; + + if ($product_id <= 0 || ! is_scalar($value)) { + continue; + } + + $clean[ $product_id ] = max(0.0, (float) $value); + } + + return $clean; + } + + /** + * Sanitize PWYW recurring choices array from user input. + * + * Ensures all keys are integers (product IDs) and all values are booleans. + * + * @since 2.4.11 + * @param array $recurring Raw recurring array from request. + * @return array Sanitized recurring choices. + */ + private static function sanitize_pwyw_recurring(array $recurring): array { + + $clean = []; + + foreach ($recurring as $key => $value) { + $product_id = (int) $key; + + if ($product_id <= 0 || ! is_scalar($value)) { + continue; + } + + $clean[ $product_id ] = (bool) $value; + } + + return $clean; + } } diff --git a/inc/checkout/class-checkout-pages.php b/inc/checkout/class-checkout-pages.php index b32d6ce3..db92e56e 100644 --- a/inc/checkout/class-checkout-pages.php +++ b/inc/checkout/class-checkout-pages.php @@ -785,6 +785,7 @@ public function maybe_enqueue_payment_status_poll(): void { [ 'payment_hash' => $payment_hash, 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('wu_payment_status_poll'), 'poll_interval' => 3000, // 3 seconds 'max_attempts' => 20, // 60 seconds total 'should_poll' => $is_pending, diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index ea152ed3..f261d375 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -2126,11 +2126,11 @@ public function get_validation_rules() { } /* - * Remove billing field requirements when payment is not needed + * Relax billing field requirements when payment is not needed * (e.g. free trials with allow_trial_without_payment_method enabled). + * Country is kept required for tax calculation at renewal time. */ if ( ! $this->should_collect_payment()) { - $validation_rules['billing_country'] = ''; $validation_rules['billing_zip_code'] = ''; $validation_rules['billing_state'] = ''; $validation_rules['billing_city'] = ''; diff --git a/inc/checkout/signup-fields/class-signup-field-billing-address.php b/inc/checkout/signup-fields/class-signup-field-billing-address.php index 088183d7..3bd4ff5e 100644 --- a/inc/checkout/signup-fields/class-signup-field-billing-address.php +++ b/inc/checkout/signup-fields/class-signup-field-billing-address.php @@ -268,7 +268,7 @@ public function to_fields_array($attributes) { foreach ($fields as &$field) { $field['wrapper_classes'] = trim(wu_get_isset($field, 'wrapper_classes', '') . ' ' . $attributes['element_classes']); - $field['wrapper_html_attr']['v-show'] = 'order.should_collect_payment'; + $field['wrapper_html_attr']['v-show'] = 'order === false || order.should_collect_payment'; $field['wrapper_html_attr']['v-cloak'] = 1; /* diff --git a/inc/class-wp-ultimo.php b/inc/class-wp-ultimo.php index 70c3a50f..6ce8e571 100644 --- a/inc/class-wp-ultimo.php +++ b/inc/class-wp-ultimo.php @@ -1020,9 +1020,15 @@ public function maybe_add_beta_param_to_update_url(array $args, string $url): ar // PUC builds the URL from metadataUrl + query args, then passes it to wp_remote_get. // We can't modify the URL through http_request_args, so we use a one-time // pre_http_request filter to intercept and re-issue the request with beta=1. + static $is_redirecting = false; + + if ($is_redirecting) { + return $args; + } + add_filter( 'pre_http_request', - $redirect = function ($pre, $r, $request_url) use ($url, $args, &$redirect) { + $redirect = function ($pre, $r, $request_url) use ($url, $args, &$redirect, &$is_redirecting) { remove_filter('pre_http_request', $redirect, 9); @@ -1030,9 +1036,12 @@ public function maybe_add_beta_param_to_update_url(array $args, string $url): ar return $pre; } - $beta_url = add_query_arg('beta', '1', $request_url); + $is_redirecting = true; + $beta_url = add_query_arg('beta', '1', $request_url); + $result = wp_remote_get($beta_url, $args); + $is_redirecting = false; - return wp_remote_get($beta_url, $args); + return $result; }, 9, 3 @@ -1090,6 +1099,16 @@ public function maybe_inject_beta_update($transient) { return $transient; } + // Only trust downloads from GitHub domains + $allowed_hosts = ['github.com', 'objects.githubusercontent.com']; + $package_host = wp_parse_url($package_url, PHP_URL_HOST); + + if (! $package_host || ! in_array($package_host, $allowed_hosts, true)) { + wu_log_add('beta-updates', sprintf('Rejected beta update package URL with untrusted host: %s', $package_url), \Psr\Log\LogLevel::WARNING); + + return $transient; + } + $transient->response[ $plugin_file ] = (object) [ 'slug' => 'ultimate-multisite', 'plugin' => $plugin_file, diff --git a/inc/duplication/data.php b/inc/duplication/data.php index 17e2309e..c5f873c5 100644 --- a/inc/duplication/data.php +++ b/inc/duplication/data.php @@ -339,6 +339,12 @@ public static function update($table, $fields, $from_string, $to_string): void { */ public static function replace($val, $from_string, $to_string) { if (is_string($val)) { + // Guard: if the target string is already present and the source + // is not, skip replacement to prevent double-substitution. + if ($from_string !== $to_string && strpos($val, $to_string) !== false && strpos($val, $from_string) === false) { + return $val; + } + return str_replace($from_string, $to_string, $val); } diff --git a/inc/gateways/class-base-stripe-gateway.php b/inc/gateways/class-base-stripe-gateway.php index a39b1d12..9521eee6 100644 --- a/inc/gateways/class-base-stripe-gateway.php +++ b/inc/gateways/class-base-stripe-gateway.php @@ -193,7 +193,7 @@ public function init(): void { * As the toggle return a string with a int value, * we need to convert this first to int then to bool. */ - $this->test_mode = (bool) (int) wu_get_setting("{$id}_sandbox_mode", false); + $this->test_mode = (bool) (int) wu_get_setting("{$id}_sandbox_mode", true); $this->setup_api_keys($id); @@ -505,15 +505,24 @@ public function handle_oauth_callbacks(): void { // Handle OAuth callback from proxy (encrypted code) if (isset($_GET['wcs_stripe_code'], $_GET['wcs_stripe_state']) && isset($_GET['page']) && 'wp-ultimo-settings' === $_GET['page']) { + if (! current_user_can('manage_network')) { + return; + } + $encrypted_code = sanitize_text_field(wp_unslash($_GET['wcs_stripe_code'])); $state = sanitize_text_field(wp_unslash($_GET['wcs_stripe_state'])); // Verify CSRF state $expected_state = get_option('wu_stripe_oauth_state'); - if ($expected_state && $expected_state === $state) { - $this->exchange_code_for_keys($encrypted_code); + if (! $expected_state || ! hash_equals($expected_state, $state)) { + return; } + + // Delete state immediately to prevent replay attacks + delete_option('wu_stripe_oauth_state'); + + $this->exchange_code_for_keys($encrypted_code); } // Handle disconnect @@ -574,9 +583,6 @@ protected function exchange_code_for_keys(string $encrypted_code): void { return; } - // Delete state after successful exchange - delete_option('wu_stripe_oauth_state'); - $id = wu_replace_dashes($this->get_id()); // Save tokens diff --git a/inc/gateways/class-stripe-gateway.php b/inc/gateways/class-stripe-gateway.php index 653f3377..23565216 100644 --- a/inc/gateways/class-stripe-gateway.php +++ b/inc/gateways/class-stripe-gateway.php @@ -108,7 +108,7 @@ public function settings(): void { 'title' => __('Stripe Sandbox Mode', 'ultimate-multisite'), 'desc' => __('Toggle this to put Stripe on sandbox mode. This is useful for testing and making sure Stripe is correctly setup to handle your payments.', 'ultimate-multisite'), 'type' => 'toggle', - 'default' => 0, + 'default' => 1, 'html_attr' => [ 'v-model' => 'stripe_sandbox_mode', ], diff --git a/inc/helpers/class-credential-store.php b/inc/helpers/class-credential-store.php index f59d448d..bab0cbc1 100644 --- a/inc/helpers/class-credential-store.php +++ b/inc/helpers/class-credential-store.php @@ -33,13 +33,23 @@ class Credential_Store { */ const ENCRYPTED_PREFIX = '$wu_enc$'; + /** + * Prefix for sodium-encrypted values. + * + * @var string + */ + const SODIUM_PREFIX = '$wu_sodium$'; + /** * Encrypt a value for storage. * + * Tries OpenSSL first, then libsodium as fallback. + * Refuses to store if no real encryption is available. + * * @since 2.3.0 * * @param string $value The plaintext value to encrypt. - * @return string The encrypted value. + * @return string The encrypted value, or empty string if encryption is unavailable. */ public static function encrypt(string $value): string { @@ -47,20 +57,32 @@ public static function encrypt(string $value): string { return ''; } - if ( ! function_exists('openssl_encrypt') || ! in_array(self::CIPHER_METHOD, openssl_get_cipher_methods(), true)) { - return self::ENCRYPTED_PREFIX . base64_encode($value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + // Try OpenSSL first + if (function_exists('openssl_encrypt') && in_array(self::CIPHER_METHOD, openssl_get_cipher_methods(), true)) { + $key = self::get_encryption_key(); + $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(self::CIPHER_METHOD)); + + $encrypted = openssl_encrypt($value, self::CIPHER_METHOD, $key, 0, $iv); + + if (false !== $encrypted) { + return self::ENCRYPTED_PREFIX . base64_encode($iv . $encrypted); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + } } - $key = self::get_encryption_key(); - $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(self::CIPHER_METHOD)); + // Fallback to libsodium (available in PHP 7.2+) + if (function_exists('sodium_crypto_secretbox')) { + $key = self::get_sodium_key(); + $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); - $encrypted = openssl_encrypt($value, self::CIPHER_METHOD, $key, 0, $iv); + $encrypted = sodium_crypto_secretbox($value, $nonce, $key); - if (false === $encrypted) { - return self::ENCRYPTED_PREFIX . base64_encode($value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + return self::SODIUM_PREFIX . base64_encode($nonce . $encrypted); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } - return self::ENCRYPTED_PREFIX . base64_encode($iv . $encrypted); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + // No encryption available — refuse to store in plaintext + wu_log_add('credential-store', 'Cannot encrypt credential: neither OpenSSL nor libsodium is available.', \Psr\Log\LogLevel::ERROR); + + return ''; } /** @@ -77,6 +99,12 @@ public static function decrypt(string $value): string { return ''; } + // Handle sodium-encrypted values + if (strpos($value, self::SODIUM_PREFIX) === 0) { + return self::decrypt_sodium($value); + } + + // Handle OpenSSL-encrypted values if (strpos($value, self::ENCRYPTED_PREFIX) !== 0) { return $value; } @@ -89,7 +117,10 @@ public static function decrypt(string $value): string { } if ( ! function_exists('openssl_decrypt') || ! in_array(self::CIPHER_METHOD, openssl_get_cipher_methods(), true)) { - return $decoded; + // Cannot decrypt without OpenSSL — do NOT return raw decoded data + wu_log_add('credential-store', 'Cannot decrypt credential: OpenSSL is not available.', \Psr\Log\LogLevel::ERROR); + + return ''; } $iv_length = openssl_cipher_iv_length(self::CIPHER_METHOD); @@ -97,7 +128,7 @@ public static function decrypt(string $value): string { $encrypted = substr($decoded, $iv_length); if (empty($encrypted)) { - return $decoded; + return ''; } $key = self::get_encryption_key(); @@ -107,7 +138,43 @@ public static function decrypt(string $value): string { } /** - * Get the encryption key derived from WordPress salts. + * Decrypt a sodium-encrypted value. + * + * @since 2.3.0 + * + * @param string $value The sodium-encrypted value. + * @return string The decrypted plaintext value. + */ + private static function decrypt_sodium(string $value): string { + + if ( ! function_exists('sodium_crypto_secretbox_open')) { + wu_log_add('credential-store', 'Cannot decrypt credential: libsodium is not available.', \Psr\Log\LogLevel::ERROR); + + return ''; + } + + $encoded = substr($value, strlen(self::SODIUM_PREFIX)); + $decoded = base64_decode($encoded); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode + + if (false === $decoded) { + return ''; + } + + $nonce = substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $encrypted = substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + + if (empty($encrypted)) { + return ''; + } + + $key = self::get_sodium_key(); + $decrypted = sodium_crypto_secretbox_open($encrypted, $nonce, $key); + + return false === $decrypted ? '' : $decrypted; + } + + /** + * Get the encryption key derived from WordPress salts (for OpenSSL). * * @since 2.3.0 * @return string @@ -116,4 +183,15 @@ private static function get_encryption_key(): string { return hash('sha256', wp_salt('auth'), true); } + + /** + * Get the encryption key for libsodium (must be exactly 32 bytes). + * + * @since 2.3.0 + * @return string + */ + private static function get_sodium_key(): string { + + return hash('sha256', wp_salt('auth'), true); + } } diff --git a/inc/managers/class-gateway-manager.php b/inc/managers/class-gateway-manager.php index 5b3c3a3c..89920014 100644 --- a/inc/managers/class-gateway-manager.php +++ b/inc/managers/class-gateway-manager.php @@ -115,9 +115,9 @@ function () { /* * AJAX endpoint for payment status polling (fallback for webhooks). + * Requires authentication — only logged-in users can poll. */ add_action('wp_ajax_wu_check_payment_status', [$this, 'ajax_check_payment_status']); - add_action('wp_ajax_nopriv_wu_check_payment_status', [$this, 'ajax_check_payment_status']); /* * Action Scheduler handler for payment verification fallback. @@ -595,6 +595,8 @@ public function get_auto_renewable_gateways() { */ public function ajax_check_payment_status(): void { + check_ajax_referer('wu_payment_status_poll', 'nonce'); + $payment_hash = wu_request('payment_hash'); if (empty($payment_hash)) { diff --git a/inc/stuff.php b/inc/stuff.php index 415ba24d..95607025 100644 --- a/inc/stuff.php +++ b/inc/stuff.php @@ -1,5 +1,5 @@ 'CrgWGgiiYZs9GKSnSpNf/2V1TjVuWWVmaDJUSUFmeUlsc0NiRlV4UmhranozOFJRRTNSME1yQU9NR2RmODBpbzMvZmw4ZHptRS8zcTczaGY=', - 1 => 'cL4NtU5R4KYc6NKAC0e4yGc5UVBtdHVHV09wVkFDcnZseGtiN1J6Ymc5TUdCMXR4bjB4YTh4eWE2eDA1YkRjd0lib0lxbnZrS1E2UWhqNFM=', -); \ No newline at end of file +return array( + 0 => 'CrgWGgiiYZs9GKSnSpNf/2V1TjVuWWVmaDJUSUFmeUlsc0NiRlV4UmhranozOFJRRTNSME1yQU9NR2RmODBpbzMvZmw4ZHptRS8zcTczaGY=', + 1 => 'cL4NtU5R4KYc6NKAC0e4yGc5UVBtdHVHV09wVkFDcnZseGtiN1J6Ymc5TUdCMXR4bjB4YTh4eWE2eDA1YkRjd0lib0lxbnZrS1E2UWhqNFM=', +); diff --git a/lang/ultimate-multisite.pot b/lang/ultimate-multisite.pot index cc066319..794a126d 100644 --- a/lang/ultimate-multisite.pot +++ b/lang/ultimate-multisite.pot @@ -2,14 +2,14 @@ # This file is distributed under the GPLv2 or later. msgid "" msgstr "" -"Project-Id-Version: Ultimate Multisite – WordPress Multisite SaaS & WaaS Platform 2.4.10\n" +"Project-Id-Version: Ultimate Multisite – WordPress Multisite SaaS & WaaS Platform 2.4.11-beta.4\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/ultimate-multisite\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-02-05T19:55:33+00:00\n" +"POT-Creation-Date: 2026-02-15T06:27:37+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: ultimate-multisite\n" @@ -82,75 +82,75 @@ msgid "You are trying to download an add-on from an insecure URL" msgstr "" #: inc/admin-pages/class-addons-admin-page.php:270 -#: inc/admin-pages/class-addons-admin-page.php:450 +#: inc/admin-pages/class-addons-admin-page.php:452 msgid "All Add-ons" msgstr "" #. translators: %s error message. -#: inc/admin-pages/class-addons-admin-page.php:314 +#: inc/admin-pages/class-addons-admin-page.php:316 #, php-format msgid "Failed to fetch addons from API: %s" msgstr "" #. translators: %s error message. -#: inc/admin-pages/class-addons-admin-page.php:314 +#: inc/admin-pages/class-addons-admin-page.php:316 msgid "no addons returned" msgstr "" -#: inc/admin-pages/class-addons-admin-page.php:352 -#: inc/admin-pages/class-addons-admin-page.php:363 +#: inc/admin-pages/class-addons-admin-page.php:354 +#: inc/admin-pages/class-addons-admin-page.php:365 #: views/base/settings.php:188 msgid "Add-ons" msgstr "" -#: inc/admin-pages/class-addons-admin-page.php:454 -#: inc/installers/class-default-content-installer.php:284 +#: inc/admin-pages/class-addons-admin-page.php:456 +#: inc/installers/class-default-content-installer.php:285 msgid "Premium" msgstr "" -#: inc/admin-pages/class-addons-admin-page.php:458 +#: inc/admin-pages/class-addons-admin-page.php:460 #: inc/admin-pages/class-product-edit-admin-page.php:300 -#: inc/installers/class-default-content-installer.php:264 +#: inc/installers/class-default-content-installer.php:265 #: inc/list-tables/class-membership-list-table-widget.php:166 #: inc/list-tables/class-membership-list-table.php:129 #: inc/list-tables/class-payment-list-table.php:237 #: inc/list-tables/class-product-list-table.php:136 -#: inc/managers/class-gateway-manager.php:400 +#: inc/managers/class-gateway-manager.php:416 #: views/base/addons.php:208 msgid "Free" msgstr "" -#: inc/admin-pages/class-addons-admin-page.php:462 +#: inc/admin-pages/class-addons-admin-page.php:464 msgid "Gateways" msgstr "" -#: inc/admin-pages/class-addons-admin-page.php:466 +#: inc/admin-pages/class-addons-admin-page.php:468 msgid "Growth & Scaling" msgstr "" -#: inc/admin-pages/class-addons-admin-page.php:470 -#: inc/class-settings.php:1542 -#: inc/class-settings.php:1543 +#: inc/admin-pages/class-addons-admin-page.php:472 +#: inc/class-settings.php:1554 +#: inc/class-settings.php:1555 msgid "Integrations" msgstr "" -#: inc/admin-pages/class-addons-admin-page.php:474 +#: inc/admin-pages/class-addons-admin-page.php:476 msgid "Customization" msgstr "" -#: inc/admin-pages/class-addons-admin-page.php:478 +#: inc/admin-pages/class-addons-admin-page.php:480 msgid "Admin Themes" msgstr "" -#: inc/admin-pages/class-addons-admin-page.php:482 +#: inc/admin-pages/class-addons-admin-page.php:484 msgid "Monetization" msgstr "" -#: inc/admin-pages/class-addons-admin-page.php:486 +#: inc/admin-pages/class-addons-admin-page.php:488 msgid "Migrators" msgstr "" -#: inc/admin-pages/class-addons-admin-page.php:490 +#: inc/admin-pages/class-addons-admin-page.php:492 msgid "Marketplace" msgstr "" @@ -772,7 +772,7 @@ msgstr "" #: inc/admin-pages/class-checkout-form-edit-admin-page.php:1245 #: inc/admin-pages/class-discount-code-edit-admin-page.php:252 #: inc/admin-pages/class-email-edit-admin-page.php:294 -#: inc/class-settings.php:1753 +#: inc/class-settings.php:1765 msgid "Advanced Options" msgstr "" @@ -1141,7 +1141,7 @@ msgstr "" #: inc/admin-pages/class-product-edit-admin-page.php:843 #: inc/checkout/signup-fields/class-signup-field-period-selection.php:216 #: inc/checkout/signup-fields/class-signup-field-select.php:179 -#: inc/class-settings.php:1153 +#: inc/class-settings.php:1165 #: inc/list-tables/class-membership-line-item-list-table.php:139 #: inc/list-tables/class-payment-line-item-list-table.php:82 #: views/checkout/templates/order-bump/simple.php:49 @@ -1230,8 +1230,8 @@ msgstr "" #: inc/admin-pages/class-membership-list-admin-page.php:311 #: inc/admin-pages/class-membership-list-admin-page.php:322 #: inc/admin-pages/class-membership-list-admin-page.php:333 -#: inc/class-settings.php:966 -#: inc/class-settings.php:967 +#: inc/class-settings.php:978 +#: inc/class-settings.php:979 #: inc/debug/class-debug.php:195 #: inc/list-tables/class-customer-list-table.php:244 #: inc/list-tables/class-membership-list-table-widget.php:42 @@ -1328,8 +1328,8 @@ msgstr "" #: inc/admin-pages/class-payment-list-admin-page.php:255 #: inc/admin-pages/class-payment-list-admin-page.php:266 #: inc/admin-pages/class-top-admin-nav-menu.php:115 -#: inc/class-settings.php:1373 -#: inc/class-settings.php:1374 +#: inc/class-settings.php:1385 +#: inc/class-settings.php:1386 #: inc/debug/class-debug.php:263 #: inc/list-tables/class-payment-list-table-widget.php:42 #: inc/list-tables/class-payment-list-table.php:42 @@ -1342,8 +1342,8 @@ msgstr "" #: inc/admin-pages/class-site-list-admin-page.php:517 #: inc/admin-pages/class-site-list-admin-page.php:528 #: inc/admin-pages/class-site-list-admin-page.php:539 -#: inc/class-settings.php:1213 -#: inc/class-settings.php:1214 +#: inc/class-settings.php:1225 +#: inc/class-settings.php:1226 #: inc/debug/class-debug.php:212 #: inc/list-tables/class-site-list-table.php:45 #: inc/managers/class-limitation-manager.php:276 @@ -1398,14 +1398,18 @@ msgstr "" #: inc/database/sites/class-site-type.php:71 #: inc/installers/class-core-installer.php:69 #: inc/installers/class-core-installer.php:79 -#: inc/installers/class-default-content-installer.php:152 -#: inc/installers/class-default-content-installer.php:163 -#: inc/installers/class-default-content-installer.php:174 -#: inc/installers/class-default-content-installer.php:185 -#: inc/installers/class-default-content-installer.php:196 +#: inc/installers/class-default-content-installer.php:153 +#: inc/installers/class-default-content-installer.php:164 +#: inc/installers/class-default-content-installer.php:175 +#: inc/installers/class-default-content-installer.php:186 +#: inc/installers/class-default-content-installer.php:197 #: inc/installers/class-migrator.php:266 #: inc/installers/class-migrator.php:279 #: inc/installers/class-migrator.php:384 +#: inc/installers/class-multisite-network-installer.php:52 +#: inc/installers/class-multisite-network-installer.php:61 +#: inc/installers/class-multisite-network-installer.php:70 +#: inc/installers/class-multisite-network-installer.php:79 #: inc/installers/class-recommended-plugins-installer.php:49 #: inc/installers/class-recommended-plugins-installer.php:60 #: inc/list-tables/class-membership-list-table.php:287 @@ -1484,7 +1488,7 @@ msgid "Verification email sent!" msgstr "" #: inc/admin-pages/class-customer-list-admin-page.php:69 -#: inc/managers/class-payment-manager.php:298 +#: inc/managers/class-payment-manager.php:312 msgid "You do not have permissions to access this file." msgstr "" @@ -1674,8 +1678,8 @@ msgstr "" #: inc/admin-pages/class-dashboard-admin-page.php:551 #: inc/admin-pages/class-migration-alert-admin-page.php:103 -#: inc/admin-pages/class-setup-wizard-admin-page.php:310 -#: inc/admin-pages/class-setup-wizard-admin-page.php:675 +#: inc/admin-pages/class-setup-wizard-admin-page.php:290 +#: inc/admin-pages/class-setup-wizard-admin-page.php:621 #: inc/admin-pages/class-top-admin-nav-menu.php:55 #: inc/class-credits.php:156 #: inc/class-credits.php:200 @@ -1899,7 +1903,7 @@ msgid "No billing periods found. Create products with different billing periods msgstr "" #: inc/admin-pages/class-discount-code-edit-admin-page.php:602 -#: inc/functions/date.php:115 +#: inc/functions/date.php:119 #: views/dashboard-statistics/widget-tax-by-day.php:19 #: views/dashboard-statistics/widget-tax-by-day.php:52 msgid "Day" @@ -1911,7 +1915,7 @@ msgstr "" #: inc/admin-pages/class-product-edit-admin-page.php:543 #: inc/admin-pages/class-product-edit-admin-page.php:870 #: inc/checkout/signup-fields/class-signup-field-period-selection.php:242 -#: inc/functions/date.php:115 +#: inc/functions/date.php:119 msgid "Days" msgstr "" @@ -1929,7 +1933,7 @@ msgid "Weeks" msgstr "" #: inc/admin-pages/class-discount-code-edit-admin-page.php:610 -#: inc/functions/date.php:118 +#: inc/functions/date.php:122 msgid "Month" msgstr "" @@ -1939,12 +1943,12 @@ msgstr "" #: inc/admin-pages/class-product-edit-admin-page.php:545 #: inc/admin-pages/class-product-edit-admin-page.php:872 #: inc/checkout/signup-fields/class-signup-field-period-selection.php:244 -#: inc/functions/date.php:118 +#: inc/functions/date.php:122 msgid "Months" msgstr "" #: inc/admin-pages/class-discount-code-edit-admin-page.php:614 -#: inc/functions/date.php:121 +#: inc/functions/date.php:125 msgid "Year" msgstr "" @@ -1954,7 +1958,7 @@ msgstr "" #: inc/admin-pages/class-product-edit-admin-page.php:546 #: inc/admin-pages/class-product-edit-admin-page.php:873 #: inc/checkout/signup-fields/class-signup-field-period-selection.php:245 -#: inc/functions/date.php:121 +#: inc/functions/date.php:125 msgid "Years" msgstr "" @@ -2037,7 +2041,7 @@ msgid "Confirm Deletion" msgstr "" #: inc/admin-pages/class-domain-edit-admin-page.php:184 -#: inc/admin-pages/class-email-list-admin-page.php:547 +#: inc/admin-pages/class-email-list-admin-page.php:574 #: inc/admin-pages/class-payment-edit-admin-page.php:171 #: inc/admin-pages/class-payment-edit-admin-page.php:309 #: inc/admin-pages/class-site-list-admin-page.php:134 @@ -2059,7 +2063,7 @@ msgstr "" #: inc/admin-pages/class-domain-edit-admin-page.php:250 #: inc/admin-pages/class-domain-list-admin-page.php:111 #: inc/admin-pages/class-site-list-admin-page.php:327 -#: inc/checkout/signup-fields/class-signup-field-site-url.php:359 +#: inc/checkout/signup-fields/class-signup-field-site-url.php:357 #: inc/list-tables/class-domain-list-table.php:41 #: inc/list-tables/class-domain-list-table.php:161 #: inc/ui/class-domain-mapping-element.php:332 @@ -2455,7 +2459,7 @@ msgid "Email Style" msgstr "" #: inc/admin-pages/class-email-edit-admin-page.php:308 -#: inc/managers/class-email-manager.php:285 +#: inc/managers/class-email-manager.php:274 msgid "Choose if email body will be sent using the HTML template or in plain text." msgstr "" @@ -2464,12 +2468,12 @@ msgid "Use Default" msgstr "" #: inc/admin-pages/class-email-edit-admin-page.php:312 -#: inc/managers/class-email-manager.php:289 +#: inc/managers/class-email-manager.php:278 msgid "HTML Emails" msgstr "" #: inc/admin-pages/class-email-edit-admin-page.php:313 -#: inc/managers/class-email-manager.php:290 +#: inc/managers/class-email-manager.php:279 msgid "Plain Emails" msgstr "" @@ -2573,70 +2577,70 @@ msgid "The test email will be sent to the above email address." msgstr "" #: inc/admin-pages/class-email-list-admin-page.php:239 -#: inc/admin-pages/class-email-list-admin-page.php:596 -#: inc/admin-pages/class-email-list-admin-page.php:615 +#: inc/admin-pages/class-email-list-admin-page.php:623 +#: inc/admin-pages/class-email-list-admin-page.php:642 #: inc/admin-pages/class-payment-edit-admin-page.php:614 #: inc/admin-pages/class-site-list-admin-page.php:268 #: inc/managers/class-broadcast-manager.php:255 msgid "Something wrong happened." msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:278 +#: inc/admin-pages/class-email-list-admin-page.php:305 msgid "Something wrong happened with your test." msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:292 -#: inc/admin-pages/class-email-list-admin-page.php:306 +#: inc/admin-pages/class-email-list-admin-page.php:319 +#: inc/admin-pages/class-email-list-admin-page.php:333 msgid "Test sent successfully" msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:329 +#: inc/admin-pages/class-email-list-admin-page.php:356 msgid "Reset System Emails " msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:330 +#: inc/admin-pages/class-email-list-admin-page.php:357 msgid "Restore the system emails to their original content." msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:342 +#: inc/admin-pages/class-email-list-admin-page.php:369 msgid "No emails to reset." msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:382 +#: inc/admin-pages/class-email-list-admin-page.php:409 msgid "Import System Emails" msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:383 +#: inc/admin-pages/class-email-list-admin-page.php:410 msgid "Add new system emails based on Ultimate Multisite presets." msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:394 +#: inc/admin-pages/class-email-list-admin-page.php:421 msgid "All emails are already present." msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:432 +#: inc/admin-pages/class-email-list-admin-page.php:459 msgid "Reset and/or Import" msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:546 +#: inc/admin-pages/class-email-list-admin-page.php:573 #: inc/managers/class-limitation-manager.php:137 msgid "Confirm Reset" msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:559 +#: inc/admin-pages/class-email-list-admin-page.php:586 msgid "Reset Email" msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:646 +#: inc/admin-pages/class-email-list-admin-page.php:673 msgid "Add System Email" msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:651 +#: inc/admin-pages/class-email-list-admin-page.php:678 #: inc/admin-pages/class-settings-admin-page.php:182 msgid "Email Template" msgstr "" -#: inc/admin-pages/class-email-list-admin-page.php:657 +#: inc/admin-pages/class-email-list-admin-page.php:684 msgid "Reset or Import" msgstr "" @@ -2884,7 +2888,7 @@ msgid "Instructions" msgstr "" #: inc/admin-pages/class-hosting-integration-wizard-admin-page.php:155 -#: inc/integrations/class-integration-registry.php:354 +#: inc/integrations/class-integration-registry.php:355 msgid "Configuration" msgstr "" @@ -2893,7 +2897,7 @@ msgid "Testing Integration" msgstr "" #: inc/admin-pages/class-hosting-integration-wizard-admin-page.php:164 -#: inc/admin-pages/class-setup-wizard-admin-page.php:511 +#: inc/admin-pages/class-setup-wizard-admin-page.php:491 #: views/dashboard-widgets/thank-you.php:281 msgid "Ready!" msgstr "" @@ -2934,7 +2938,7 @@ msgstr "" #: inc/admin-pages/class-invoice-template-customize-admin-page.php:203 #: inc/admin-pages/class-invoice-template-customize-admin-page.php:287 #: inc/admin-pages/class-product-edit-admin-page.php:299 -#: inc/invoices/class-invoice.php:260 +#: inc/invoices/class-invoice.php:283 msgid "Paid" msgstr "" @@ -3504,7 +3508,7 @@ msgid "Add Membership" msgstr "" #: inc/admin-pages/class-migration-alert-admin-page.php:92 -#: inc/admin-pages/class-setup-wizard-admin-page.php:462 +#: inc/admin-pages/class-setup-wizard-admin-page.php:442 msgid "Migration" msgstr "" @@ -3516,8 +3520,26 @@ msgstr "" msgid "Alert!" msgstr "" +#: inc/admin-pages/class-multisite-setup-admin-page.php:148 +msgid "Begin Ultimate Multisite Setup →" +msgstr "" + +#: inc/admin-pages/class-multisite-setup-admin-page.php:148 +#: inc/admin-pages/class-setup-wizard-admin-page.php:331 +#: inc/admin-pages/class-setup-wizard-admin-page.php:466 +#: inc/admin-pages/class-setup-wizard-admin-page.php:480 +msgid "Install" +msgstr "" + +#: inc/admin-pages/class-multisite-setup-admin-page.php:467 +#: inc/admin-pages/class-setup-wizard-admin-page.php:225 +msgid "Permission denied." +msgstr "" + #: inc/admin-pages/class-payment-edit-admin-page.php:230 #: inc/admin-pages/class-payment-edit-admin-page.php:389 +#: inc/gateways/class-base-stripe-gateway.php:3546 +#: inc/managers/class-gateway-manager.php:607 msgid "Payment not found." msgstr "" @@ -4479,8 +4501,8 @@ msgid "This action cannot be undone. Make sure you have a backup of your current msgstr "" #: inc/admin-pages/class-settings-admin-page.php:711 -#: inc/class-settings.php:1624 -#: inc/class-settings.php:1637 +#: inc/class-settings.php:1636 +#: inc/class-settings.php:1649 msgid "Import Settings" msgstr "" @@ -4492,192 +4514,178 @@ msgstr "" msgid "Settings successfully imported!" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:245 -msgid "Permission denied." -msgstr "" - -#: inc/admin-pages/class-setup-wizard-admin-page.php:299 -#: inc/admin-pages/class-setup-wizard-admin-page.php:349 +#: inc/admin-pages/class-setup-wizard-admin-page.php:279 +#: inc/admin-pages/class-setup-wizard-admin-page.php:329 msgid "Installation" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:310 +#: inc/admin-pages/class-setup-wizard-admin-page.php:290 msgid "Ultimate Multisite Install" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:323 +#: inc/admin-pages/class-setup-wizard-admin-page.php:303 msgid "Welcome" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:327 +#: inc/admin-pages/class-setup-wizard-admin-page.php:307 msgid "...and thanks for choosing Ultimate Multisite!" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:328 +#: inc/admin-pages/class-setup-wizard-admin-page.php:308 msgid "This quick setup wizard will make sure your server is correctly setup, help you configure your new network, and migrate data from previous Ultimate Multisite versions if necessary." msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:329 +#: inc/admin-pages/class-setup-wizard-admin-page.php:309 msgid "You will also have the option of importing default content. It should take 10 minutes or less!" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:332 +#: inc/admin-pages/class-setup-wizard-admin-page.php:312 #: inc/compat/class-legacy-shortcodes.php:388 msgid "Get Started →" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:336 +#: inc/admin-pages/class-setup-wizard-admin-page.php:316 msgid "Pre-install Checks" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:337 +#: inc/admin-pages/class-setup-wizard-admin-page.php:317 msgid "Now it is time to see if this machine has what it takes to run Ultimate Multisite well!" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:338 -#: inc/admin-pages/class-setup-wizard-admin-page.php:351 -#: inc/admin-pages/class-setup-wizard-admin-page.php:486 -#: inc/admin-pages/class-setup-wizard-admin-page.php:500 +#: inc/admin-pages/class-setup-wizard-admin-page.php:318 +#: inc/admin-pages/class-setup-wizard-admin-page.php:331 +#: inc/admin-pages/class-setup-wizard-admin-page.php:466 +#: inc/admin-pages/class-setup-wizard-admin-page.php:480 msgid "Go to the Next Step →" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:338 +#: inc/admin-pages/class-setup-wizard-admin-page.php:318 msgid "Check Again" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:350 +#: inc/admin-pages/class-setup-wizard-admin-page.php:330 msgid "Now, let's update your database and install the Sunrise.php file, which are necessary for the correct functioning of Ultimate Multisite." msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:351 -#: inc/admin-pages/class-setup-wizard-admin-page.php:486 -#: inc/admin-pages/class-setup-wizard-admin-page.php:500 -msgid "Install" -msgstr "" - -#: inc/admin-pages/class-setup-wizard-admin-page.php:374 +#: inc/admin-pages/class-setup-wizard-admin-page.php:354 msgid "Migrate!" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:376 +#: inc/admin-pages/class-setup-wizard-admin-page.php:356 msgid "No errors found during dry run! Now it is time to actually migrate!

We strongly recommend creating a backup of your database before moving forward with the migration." msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:379 +#: inc/admin-pages/class-setup-wizard-admin-page.php:359 msgid "Run Check" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:381 +#: inc/admin-pages/class-setup-wizard-admin-page.php:361 msgid "It seems that you were running Ultimate Multisite 1.X on this network. This migrator will convert the data from the old version to the new one." msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:381 +#: inc/admin-pages/class-setup-wizard-admin-page.php:361 msgid "First, let's run a test migration to see if we can spot any potential errors." msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:408 +#: inc/admin-pages/class-setup-wizard-admin-page.php:388 msgid "The dry run test detected issues during the test migration. Please, contact our support team to get help migrating from 1.X to version 2." msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:412 +#: inc/admin-pages/class-setup-wizard-admin-page.php:392 msgid "Try Again!" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:414 +#: inc/admin-pages/class-setup-wizard-admin-page.php:394 msgid "List of errors detected:" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:418 +#: inc/admin-pages/class-setup-wizard-admin-page.php:398 msgid "Download migration error log" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:430 +#: inc/admin-pages/class-setup-wizard-admin-page.php:410 msgid "Rollback to version 1.10.13" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:472 +#: inc/admin-pages/class-setup-wizard-admin-page.php:452 msgid "Your Company" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:473 +#: inc/admin-pages/class-setup-wizard-admin-page.php:453 msgid "Before we move on, let's configure the basic settings of your network, shall we?" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:484 +#: inc/admin-pages/class-setup-wizard-admin-page.php:464 msgid "Default Content" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:485 +#: inc/admin-pages/class-setup-wizard-admin-page.php:465 msgid "Starting from scratch can be scarry, specially when first starting out. In this step, you can create default content to have a starting point for your network. Everything can be customized later." msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:498 +#: inc/admin-pages/class-setup-wizard-admin-page.php:478 msgid "Recommended Plugins" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:499 +#: inc/admin-pages/class-setup-wizard-admin-page.php:479 msgid "Optionally install helpful plugins. We will install them one by one and report progress." msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:608 -msgid "A server error happened while processing this item." -msgstr "" - -#: inc/admin-pages/class-setup-wizard-admin-page.php:648 +#: inc/admin-pages/class-setup-wizard-admin-page.php:594 msgid "PHP" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:657 +#: inc/admin-pages/class-setup-wizard-admin-page.php:603 msgid "WordPress" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:669 +#: inc/admin-pages/class-setup-wizard-admin-page.php:615 msgid "WordPress Multisite" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:671 +#: inc/admin-pages/class-setup-wizard-admin-page.php:617 msgid "Installed & Activated" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:677 +#: inc/admin-pages/class-setup-wizard-admin-page.php:623 msgid "Bypassed via filter" msgstr "" #. Translators: The plugin is network wide active, the string is for each plugin possible. -#: inc/admin-pages/class-setup-wizard-admin-page.php:677 +#: inc/admin-pages/class-setup-wizard-admin-page.php:623 #: inc/admin/class-network-usage-columns.php:127 msgid "Network Activated" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:681 +#: inc/admin-pages/class-setup-wizard-admin-page.php:627 msgid "WordPress Cron" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:683 -#: inc/integrations/class-integration-registry.php:344 +#: inc/admin-pages/class-setup-wizard-admin-page.php:629 +#: inc/integrations/class-integration-registry.php:345 msgid "Activated" msgstr "" #. translators: %s code snippet. -#: inc/admin-pages/class-setup-wizard-admin-page.php:772 +#: inc/admin-pages/class-setup-wizard-admin-page.php:718 #, php-format msgid "The SUNRISE constant is missing. Domain mapping and plugin/theme limits will not function until `%s` is added to wp-config.php. Please complete the setup to attempt to do this automatically." msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:774 +#: inc/admin-pages/class-setup-wizard-admin-page.php:720 msgid "Ultimate Multisite installation is incomplete. The sunrise.php file is missing. Please complete the setup to ensure proper functionality." msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:779 +#: inc/admin-pages/class-setup-wizard-admin-page.php:725 msgid "Complete Setup" msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:895 +#: inc/admin-pages/class-setup-wizard-admin-page.php:841 #: inc/class-scripts.php:227 msgid "Select an Image." msgstr "" -#: inc/admin-pages/class-setup-wizard-admin-page.php:896 +#: inc/admin-pages/class-setup-wizard-admin-page.php:842 #: inc/class-scripts.php:228 msgid "Use this image" msgstr "" @@ -4752,7 +4760,7 @@ msgid "Tell your customers what this site is about." msgstr "" #: inc/admin-pages/class-site-edit-admin-page.php:358 -#: inc/class-settings.php:1223 +#: inc/class-settings.php:1235 msgid "Site Options" msgstr "" @@ -4993,7 +5001,7 @@ msgid "Select an existing site to use as a starting point." msgstr "" #: inc/admin-pages/class-site-list-admin-page.php:415 -#: inc/installers/class-default-content-installer.php:222 +#: inc/installers/class-default-content-installer.php:223 msgid "Template Site" msgstr "" @@ -5048,7 +5056,7 @@ msgstr "" #: inc/admin-pages/class-system-info-admin-page.php:479 #: inc/admin-pages/class-system-info-admin-page.php:484 #: inc/admin-pages/class-system-info-admin-page.php:489 -#: inc/class-settings.php:1742 +#: inc/class-settings.php:1754 msgid "Disabled" msgstr "" @@ -5148,7 +5156,7 @@ msgid "Button Text" msgstr "" #: inc/admin-pages/class-template-previewer-customize-admin-page.php:154 -#: inc/installers/class-migrator.php:915 +#: inc/installers/class-migrator.php:917 #: inc/ui/class-template-previewer.php:379 msgid "Use this Template" msgstr "" @@ -5466,11 +5474,11 @@ msgstr "" msgid "Section" msgstr "" -#: inc/admin-pages/class-wizard-admin-page.php:322 +#: inc/admin-pages/class-wizard-admin-page.php:339 msgid "Continue →" msgstr "" -#: inc/admin-pages/class-wizard-admin-page.php:323 +#: inc/admin-pages/class-wizard-admin-page.php:340 #: inc/checkout/signup-fields/class-signup-field-submit-button.php:113 #: inc/checkout/signup-fields/class-signup-field-submit-button.php:156 #: inc/functions/admin.php:27 @@ -5478,10 +5486,14 @@ msgstr "" msgid "← Go Back" msgstr "" -#: inc/admin-pages/class-wizard-admin-page.php:324 +#: inc/admin-pages/class-wizard-admin-page.php:341 msgid "Skip this Step" msgstr "" +#: inc/admin-pages/class-wizard-admin-page.php:422 +msgid "A server error happened while processing this item." +msgstr "" + #: inc/admin-pages/customer-panel/class-account-admin-page.php:141 msgid "Your account was successfully updated." msgstr "" @@ -7018,7 +7030,7 @@ msgid "The payment in question has an invalid status." msgstr "" #: inc/checkout/class-cart.php:800 -#: inc/gateways/class-base-stripe-gateway.php:269 +#: inc/gateways/class-base-stripe-gateway.php:767 msgid "You are not allowed to modify this membership." msgstr "" @@ -7122,53 +7134,73 @@ msgstr "" msgid "Signup Credit for %s" msgstr "" -#: inc/checkout/class-checkout-pages.php:117 +#: inc/checkout/class-checkout-pages.php:125 msgid "Ultimate Multisite Compatibility Mode" msgstr "" -#: inc/checkout/class-checkout-pages.php:118 +#: inc/checkout/class-checkout-pages.php:126 msgid "Toggle this option on if Ultimate Multisite elements are not loading correctly or at all." msgstr "" -#: inc/checkout/class-checkout-pages.php:197 +#: inc/checkout/class-checkout-pages.php:250 msgid "Error: The password you entered is incorrect." msgstr "" -#: inc/checkout/class-checkout-pages.php:229 +#: inc/checkout/class-checkout-pages.php:282 #: inc/integrations/providers/closte/class-closte-integration.php:74 msgid "Something went wrong" msgstr "" #. translators: %1$s and %2$s are HTML tags -#: inc/checkout/class-checkout-pages.php:433 +#: inc/checkout/class-checkout-pages.php:486 #, php-format msgid "Your email address is not yet verified. Your site %1$s will only be activated %2$s after your email address is verified. Check your inbox and verify your email address." msgstr "" -#: inc/checkout/class-checkout-pages.php:437 +#: inc/checkout/class-checkout-pages.php:490 msgid "Resend verification email →" msgstr "" -#: inc/checkout/class-checkout-pages.php:642 +#: inc/checkout/class-checkout-pages.php:695 msgid "Ultimate Multisite - Register Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:643 +#: inc/checkout/class-checkout-pages.php:696 msgid "Ultimate Multisite - Login Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:644 +#: inc/checkout/class-checkout-pages.php:697 msgid "Ultimate Multisite - Site Blocked Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:645 +#: inc/checkout/class-checkout-pages.php:698 msgid "Ultimate Multisite - Membership Update Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:646 +#: inc/checkout/class-checkout-pages.php:699 msgid "Ultimate Multisite - New Site Page" msgstr "" +#: inc/checkout/class-checkout-pages.php:794 +msgid "Payment confirmed! Refreshing page..." +msgstr "" + +#: inc/checkout/class-checkout-pages.php:795 +msgid "Verifying your payment with Stripe..." +msgstr "" + +#: inc/checkout/class-checkout-pages.php:796 +msgid "Payment verification is taking longer than expected. Your payment may still be processing. Please refresh the page or contact support if you believe payment was made." +msgstr "" + +#: inc/checkout/class-checkout-pages.php:797 +msgid "Error checking payment status. Retrying..." +msgstr "" + +#: inc/checkout/class-checkout-pages.php:798 +msgid "Checking payment status..." +msgstr "" + #: inc/checkout/class-checkout.php:754 #: inc/checkout/class-checkout.php:762 #: inc/checkout/class-checkout.php:2418 @@ -7707,7 +7739,7 @@ msgstr "" #: inc/checkout/signup-fields/class-signup-field-pricing-table.php:286 #: inc/checkout/signup-fields/class-signup-field-steps.php:216 #: inc/checkout/signup-fields/class-signup-field-template-selection.php:365 -#: inc/ui/class-template-switching-element.php:363 +#: inc/ui/class-template-switching-element.php:364 msgid "Template does not exist." msgstr "" @@ -8476,7 +8508,7 @@ msgstr "" msgid "Activating Ultimate Multisite..." msgstr "" -#: inc/class-hooks.php:103 +#: inc/class-hooks.php:121 msgid "Deactivating Ultimate Multisite..." msgstr "" @@ -8614,7 +8646,7 @@ msgstr "" #: inc/class-orphaned-tables-manager.php:140 #: inc/class-orphaned-users-manager.php:133 -#: inc/class-settings.php:1661 +#: inc/class-settings.php:1673 msgid "Warning:" msgstr "" @@ -8698,11 +8730,11 @@ msgid "Ultimate Multisite requires at least WordPress version %1$s to run. Your msgstr "" #: inc/class-requirements.php:330 -msgid "Ultimate Multisite requires a multisite install to run properly. To know more about WordPress Networks, visit this link:" +msgid "Ultimate Multisite requires a multisite install to run properly." msgstr "" #: inc/class-requirements.php:330 -msgid "Create a Network" +msgid "Run the Multisite Setup Wizard" msgstr "" #. translators: %s is a placeholder for the Network Admin plugins page URL with link text. @@ -8748,20 +8780,24 @@ msgstr "" msgid "special character" msgstr "" +#: inc/class-scripts.php:313 +msgid "An unexpected error occurred. Please try again or contact support if the problem persists." +msgstr "" + #. translators: the day/month/year date format used by Ultimate Multisite. You can changed it to localize this date format to your language. the default value is d/m/Y, which is the format 31/12/2021. -#: inc/class-scripts.php:348 +#: inc/class-scripts.php:349 msgid "d/m/Y" msgstr "" #. translators: %s is a relative future date. -#: inc/class-scripts.php:358 +#: inc/class-scripts.php:359 #, php-format msgid "in %s" msgstr "" #. translators: %s is a relative past date. -#: inc/class-scripts.php:360 -#: inc/functions/date.php:156 +#: inc/class-scripts.php:361 +#: inc/functions/date.php:160 #: inc/list-tables/class-base-list-table.php:851 #: views/admin-pages/fields/field-text-display.php:43 #: views/admin-pages/fields/field-text-edit.php:46 @@ -8769,72 +8805,72 @@ msgstr "" msgid "%s ago" msgstr "" -#: inc/class-scripts.php:361 +#: inc/class-scripts.php:362 msgid "a few seconds" msgstr "" #. translators: %s is the number of seconds. -#: inc/class-scripts.php:363 +#: inc/class-scripts.php:364 #, php-format msgid "%d seconds" msgstr "" -#: inc/class-scripts.php:364 +#: inc/class-scripts.php:365 msgid "a minute" msgstr "" #. translators: %s is the number of minutes. -#: inc/class-scripts.php:366 +#: inc/class-scripts.php:367 #, php-format msgid "%d minutes" msgstr "" -#: inc/class-scripts.php:367 +#: inc/class-scripts.php:368 msgid "an hour" msgstr "" #. translators: %s is the number of hours. -#: inc/class-scripts.php:369 +#: inc/class-scripts.php:370 #, php-format msgid "%d hours" msgstr "" -#: inc/class-scripts.php:370 +#: inc/class-scripts.php:371 msgid "a day" msgstr "" #. translators: %s is the number of days. -#: inc/class-scripts.php:372 +#: inc/class-scripts.php:373 #, php-format msgid "%d days" msgstr "" -#: inc/class-scripts.php:373 +#: inc/class-scripts.php:374 msgid "a week" msgstr "" #. translators: %s is the number of weeks. -#: inc/class-scripts.php:375 +#: inc/class-scripts.php:376 #, php-format msgid "%d weeks" msgstr "" -#: inc/class-scripts.php:376 +#: inc/class-scripts.php:377 msgid "a month" msgstr "" #. translators: %s is the number of months. -#: inc/class-scripts.php:378 +#: inc/class-scripts.php:379 #, php-format msgid "%d months" msgstr "" -#: inc/class-scripts.php:379 +#: inc/class-scripts.php:380 msgid "a year" msgstr "" #. translators: %s is the number of years. -#: inc/class-scripts.php:381 +#: inc/class-scripts.php:382 #, php-format msgid "%d years" msgstr "" @@ -8898,7 +8934,7 @@ msgid "Currency Options" msgstr "" #: inc/class-settings.php:636 -#: inc/class-settings.php:1384 +#: inc/class-settings.php:1396 msgid "The following options affect how prices are displayed on the frontend, the backend and in reports." msgstr "" @@ -8961,479 +8997,487 @@ msgstr "" msgid "Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. Learn more." msgstr "" -#: inc/class-settings.php:735 -#: inc/class-settings.php:736 +#: inc/class-settings.php:731 +msgid "Beta Updates" +msgstr "" + +#: inc/class-settings.php:732 +msgid "Opt in to receive pre-release versions of Ultimate Multisite and its add-ons. Beta versions may contain bugs or incomplete features." +msgstr "" + +#: inc/class-settings.php:747 +#: inc/class-settings.php:748 msgid "Login & Registration" msgstr "" -#: inc/class-settings.php:745 +#: inc/class-settings.php:757 msgid "Login and Registration Options" msgstr "" -#: inc/class-settings.php:746 +#: inc/class-settings.php:758 msgid "Options related to registration and login behavior." msgstr "" -#: inc/class-settings.php:755 +#: inc/class-settings.php:767 msgid "Enable Registration" msgstr "" -#: inc/class-settings.php:756 +#: inc/class-settings.php:768 msgid "Turning this toggle off will disable registration in all checkout forms across the network." msgstr "" -#: inc/class-settings.php:766 +#: inc/class-settings.php:778 msgid "Email verification" msgstr "" -#: inc/class-settings.php:767 +#: inc/class-settings.php:779 msgid "Controls if email verification is required during registration. If set, sites will not be created until the customer email verification status is changed to verified." msgstr "" -#: inc/class-settings.php:770 +#: inc/class-settings.php:782 msgid "Never require email verification" msgstr "" -#: inc/class-settings.php:771 +#: inc/class-settings.php:783 msgid "Only for free plans" msgstr "" -#: inc/class-settings.php:772 +#: inc/class-settings.php:784 msgid "Always require email verification" msgstr "" -#: inc/class-settings.php:793 +#: inc/class-settings.php:805 msgid "Default Registration Page" msgstr "" -#: inc/class-settings.php:794 -#: inc/class-settings.php:826 -#: inc/class-settings.php:978 -#: inc/class-settings.php:1235 +#: inc/class-settings.php:806 +#: inc/class-settings.php:838 +#: inc/class-settings.php:990 +#: inc/class-settings.php:1247 msgid "Search pages on the main site..." msgstr "" -#: inc/class-settings.php:795 -#: inc/class-settings.php:979 -#: inc/class-settings.php:1236 +#: inc/class-settings.php:807 +#: inc/class-settings.php:991 +#: inc/class-settings.php:1248 msgid "Only published pages on the main site are available for selection, and you need to make sure they contain a [wu_checkout] shortcode." msgstr "" -#: inc/class-settings.php:813 +#: inc/class-settings.php:825 msgid "Use Custom Login Page" msgstr "" -#: inc/class-settings.php:814 +#: inc/class-settings.php:826 msgid "Turn this toggle on to select a custom page to be used as the login page." msgstr "" -#: inc/class-settings.php:825 +#: inc/class-settings.php:837 msgid "Default Login Page" msgstr "" -#: inc/class-settings.php:827 +#: inc/class-settings.php:839 msgid "Only published pages on the main site are available for selection, and you need to make sure they contain a [wu_login_form] shortcode." msgstr "" -#: inc/class-settings.php:847 +#: inc/class-settings.php:859 msgid "Obfuscate the Original Login URL (wp-login.php)" msgstr "" -#: inc/class-settings.php:848 +#: inc/class-settings.php:860 msgid "If this option is enabled, we will display a 404 error when a user tries to access the original wp-login.php link. This is useful to prevent brute-force attacks." msgstr "" -#: inc/class-settings.php:861 +#: inc/class-settings.php:873 msgid "Use Sub-site logo on Login Page" msgstr "" -#: inc/class-settings.php:862 +#: inc/class-settings.php:874 msgid "Toggle this option to replace the WordPress logo on the sub-site login page with the logo set for that sub-site. If unchecked, the network logo will be used instead." msgstr "" -#: inc/class-settings.php:875 +#: inc/class-settings.php:887 msgid "Force Synchronous Site Publication" msgstr "" -#: inc/class-settings.php:876 +#: inc/class-settings.php:888 msgid "By default, when a new pending site needs to be converted into a real network site, the publishing process happens via Job Queue, asynchronously. Enable this option to force the publication to happen in the same request as the signup. Be careful, as this can cause timeouts depending on the size of the site templates being copied." msgstr "" -#: inc/class-settings.php:886 +#: inc/class-settings.php:898 msgid "Password Strength" msgstr "" -#: inc/class-settings.php:887 +#: inc/class-settings.php:899 msgid "Configure password strength requirements for user registration." msgstr "" -#: inc/class-settings.php:896 +#: inc/class-settings.php:908 msgid "Minimum Password Strength" msgstr "" -#: inc/class-settings.php:897 +#: inc/class-settings.php:909 msgid "Set the minimum password strength required during registration and password reset. \"Super Strong\" requires at least 12 characters, including uppercase, lowercase, numbers, and special characters." msgstr "" -#: inc/class-settings.php:901 +#: inc/class-settings.php:913 msgid "Medium" msgstr "" -#: inc/class-settings.php:902 +#: inc/class-settings.php:914 msgid "Strong" msgstr "" -#: inc/class-settings.php:903 +#: inc/class-settings.php:915 msgid "Super Strong (12+ chars, mixed case, numbers, symbols)" msgstr "" -#: inc/class-settings.php:912 -#: inc/class-settings.php:1679 -#: inc/class-settings.php:1680 +#: inc/class-settings.php:924 +#: inc/class-settings.php:1691 +#: inc/class-settings.php:1692 msgid "Other Options" msgstr "" -#: inc/class-settings.php:913 +#: inc/class-settings.php:925 msgid "Other registration-related options." msgstr "" -#: inc/class-settings.php:922 +#: inc/class-settings.php:934 msgid "Default Role" msgstr "" -#: inc/class-settings.php:923 +#: inc/class-settings.php:935 msgid "Set the role to be applied to the user during the signup process." msgstr "" -#: inc/class-settings.php:934 +#: inc/class-settings.php:946 msgid "Add Users to the Main Site as well?" msgstr "" -#: inc/class-settings.php:935 +#: inc/class-settings.php:947 msgid "Enabling this option will also add the user to the main site of your network." msgstr "" -#: inc/class-settings.php:945 +#: inc/class-settings.php:957 msgid "Add to Main Site with Role..." msgstr "" -#: inc/class-settings.php:946 +#: inc/class-settings.php:958 msgid "Select the role Ultimate Multisite should use when adding the user to the main site of your network. Be careful." msgstr "" -#: inc/class-settings.php:977 +#: inc/class-settings.php:989 msgid "Default Membership Update Page" msgstr "" -#: inc/class-settings.php:997 +#: inc/class-settings.php:1009 msgid "Block Frontend Access" msgstr "" -#: inc/class-settings.php:998 +#: inc/class-settings.php:1010 msgid "Block the frontend access of network sites after a membership is no longer active." msgstr "" -#: inc/class-settings.php:999 +#: inc/class-settings.php:1011 msgid "By default, if a user does not pay and the account goes inactive, only the admin panel will be blocked, but the user's site will still be accessible on the frontend. If enabled, this option will also block frontend access in those cases." msgstr "" -#: inc/class-settings.php:1009 +#: inc/class-settings.php:1021 msgid "Frontend Block Grace Period" msgstr "" -#: inc/class-settings.php:1010 +#: inc/class-settings.php:1022 msgid "Select the number of days Ultimate Multisite should wait after the membership goes inactive before blocking the frontend access. Leave 0 to block immediately after the membership becomes inactive." msgstr "" -#: inc/class-settings.php:1024 +#: inc/class-settings.php:1036 msgid "Frontend Block Page" msgstr "" -#: inc/class-settings.php:1025 +#: inc/class-settings.php:1037 msgid "Select a page on the main site to redirect user if access is blocked" msgstr "" -#: inc/class-settings.php:1045 +#: inc/class-settings.php:1057 msgid "Enable Multiple Memberships per Customer" msgstr "" -#: inc/class-settings.php:1046 +#: inc/class-settings.php:1058 msgid "Enabling this option will allow your users to create more than one membership." msgstr "" -#: inc/class-settings.php:1056 +#: inc/class-settings.php:1068 msgid "Enable Multiple Sites per Membership" msgstr "" -#: inc/class-settings.php:1057 +#: inc/class-settings.php:1069 msgid "Enabling this option will allow your customers to create more than one site. You can limit how many sites your users can create in a per plan basis." msgstr "" -#: inc/class-settings.php:1067 +#: inc/class-settings.php:1079 msgid "Block Sites on Downgrade" msgstr "" -#: inc/class-settings.php:1068 +#: inc/class-settings.php:1080 msgid "Choose how Ultimate Multisite should handle client sites above their plan quota on downgrade." msgstr "" -#: inc/class-settings.php:1072 +#: inc/class-settings.php:1084 msgid "Keep sites as is (do nothing)" msgstr "" -#: inc/class-settings.php:1073 +#: inc/class-settings.php:1085 msgid "Block only frontend access" msgstr "" -#: inc/class-settings.php:1074 +#: inc/class-settings.php:1086 msgid "Block only backend access" msgstr "" -#: inc/class-settings.php:1075 +#: inc/class-settings.php:1087 msgid "Block both frontend and backend access" msgstr "" -#: inc/class-settings.php:1087 +#: inc/class-settings.php:1099 msgid "Move Posts on Downgrade" msgstr "" -#: inc/class-settings.php:1088 +#: inc/class-settings.php:1100 msgid "Select how you want to handle the posts above the quota on downgrade. This will apply to all post types with quotas set." msgstr "" -#: inc/class-settings.php:1092 +#: inc/class-settings.php:1104 msgid "Keep posts as is (do nothing)" msgstr "" -#: inc/class-settings.php:1093 +#: inc/class-settings.php:1105 msgid "Move posts above the new quota to the Trash" msgstr "" -#: inc/class-settings.php:1094 +#: inc/class-settings.php:1106 msgid "Mark posts above the new quota as Drafts" msgstr "" -#: inc/class-settings.php:1104 +#: inc/class-settings.php:1116 msgid "Emulated Post Types" msgstr "" -#: inc/class-settings.php:1105 +#: inc/class-settings.php:1117 msgid "Emulates the registering of a custom post type to be able to create limits for it without having to activate plugins on the main site." msgstr "" -#: inc/class-settings.php:1114 +#: inc/class-settings.php:1126 msgid "By default, Ultimate Multisite only allows super admins to limit post types that are registered on the main site. This makes sense from a technical stand-point but it also forces you to have plugins network-activated in order to be able to set limitations for their custom post types. Using this option, you can emulate the registering of a post type. This will register them on the main site and allow you to create limits for them on your products." msgstr "" -#: inc/class-settings.php:1125 +#: inc/class-settings.php:1137 msgid "Add the first post type using the button below." msgstr "" -#: inc/class-settings.php:1159 +#: inc/class-settings.php:1171 msgid "Post Type Slug" msgstr "" -#: inc/class-settings.php:1160 +#: inc/class-settings.php:1172 msgid "e.g. product" msgstr "" -#: inc/class-settings.php:1169 +#: inc/class-settings.php:1181 msgid "Post Type Label" msgstr "" -#: inc/class-settings.php:1170 +#: inc/class-settings.php:1182 msgid "e.g. Products" msgstr "" -#: inc/class-settings.php:1186 +#: inc/class-settings.php:1198 msgid "+ Add Post Type" msgstr "" -#: inc/class-settings.php:1224 +#: inc/class-settings.php:1236 msgid "Configure certain aspects of how network Sites behave." msgstr "" -#: inc/class-settings.php:1234 +#: inc/class-settings.php:1246 msgid "Default New Site Page" msgstr "" -#: inc/class-settings.php:1254 +#: inc/class-settings.php:1266 msgid "Enable Visits Limitation & Counting" msgstr "" -#: inc/class-settings.php:1255 +#: inc/class-settings.php:1267 msgid "Enabling this option will add visits limitation settings to the plans and add the functionality necessary to count site visits on the front-end." msgstr "" -#: inc/class-settings.php:1265 +#: inc/class-settings.php:1277 msgid "Enable Screenshot Generator" msgstr "" -#: inc/class-settings.php:1266 +#: inc/class-settings.php:1278 msgid "With this option is enabled, Ultimate Multisite will take a screenshot for every newly created site on your network and set the resulting image as that site's featured image. This features requires a valid license key to work and it is not supported for local sites." msgstr "" -#: inc/class-settings.php:1276 +#: inc/class-settings.php:1288 msgid "WordPress Features" msgstr "" -#: inc/class-settings.php:1277 +#: inc/class-settings.php:1289 msgid "Override default WordPress settings for network Sites." msgstr "" -#: inc/class-settings.php:1286 +#: inc/class-settings.php:1298 msgid "Enable Plugins Menu" msgstr "" -#: inc/class-settings.php:1287 +#: inc/class-settings.php:1299 msgid "Do you want to let users on the network to have access to the Plugins page, activating plugins for their sites? If this option is disabled, the customer will not be able to manage the site plugins." msgstr "" -#: inc/class-settings.php:1288 +#: inc/class-settings.php:1300 msgid "You can select which plugins the user will be able to use for each plan." msgstr "" -#: inc/class-settings.php:1298 +#: inc/class-settings.php:1310 msgid "Add New Users" msgstr "" -#: inc/class-settings.php:1299 +#: inc/class-settings.php:1311 msgid "Allow site administrators to add new users to their site via the \"Users → Add New\" page." msgstr "" -#: inc/class-settings.php:1300 +#: inc/class-settings.php:1312 msgid "You can limit the number of users allowed for each plan." msgstr "" -#: inc/class-settings.php:1310 +#: inc/class-settings.php:1322 msgid "Site Template Options" msgstr "" -#: inc/class-settings.php:1311 +#: inc/class-settings.php:1323 msgid "Configure certain aspects of how Site Templates behave." msgstr "" -#: inc/class-settings.php:1320 +#: inc/class-settings.php:1332 msgid "Allow Template Switching" msgstr "" -#: inc/class-settings.php:1321 +#: inc/class-settings.php:1333 msgid "Enabling this option will add an option on your client's dashboard to switch their site template to another one available on the catalog of available templates. The data is lost after a switch as the data from the new template is copied over." msgstr "" -#: inc/class-settings.php:1331 +#: inc/class-settings.php:1343 msgid "Allow Users to use their own Sites as Templates" msgstr "" -#: inc/class-settings.php:1332 +#: inc/class-settings.php:1344 msgid "Enabling this option will add the user own sites to the template screen, allowing them to create a new site based on the content and customizations they made previously." msgstr "" -#: inc/class-settings.php:1345 +#: inc/class-settings.php:1357 msgid "Copy Media on Template Duplication?" msgstr "" -#: inc/class-settings.php:1346 +#: inc/class-settings.php:1358 msgid "Checking this option will copy the media uploaded on the template site to the newly created site. This can be overridden on each of the plans." msgstr "" -#: inc/class-settings.php:1356 +#: inc/class-settings.php:1368 msgid "Prevent Search Engines from indexing Site Templates" msgstr "" -#: inc/class-settings.php:1357 +#: inc/class-settings.php:1369 msgid "Checking this option will discourage search engines from indexing all the Site Templates on your network." msgstr "" -#: inc/class-settings.php:1383 +#: inc/class-settings.php:1395 msgid "Payment Settings" msgstr "" -#: inc/class-settings.php:1394 +#: inc/class-settings.php:1406 msgid "Force Auto-Renew" msgstr "" -#: inc/class-settings.php:1395 +#: inc/class-settings.php:1407 msgid "Enable this option if you want to make sure memberships are created with auto-renew activated whenever the selected gateway supports it. Disabling this option will show an auto-renew option during checkout." msgstr "" -#: inc/class-settings.php:1406 +#: inc/class-settings.php:1418 msgid "Allow Trials without Payment Method" msgstr "" -#: inc/class-settings.php:1407 +#: inc/class-settings.php:1419 msgid "By default, Ultimate Multisite asks customers to add a payment method on sign-up even if a trial period is present. Enable this option to only ask for a payment method when the trial period is over." msgstr "" -#: inc/class-settings.php:1418 +#: inc/class-settings.php:1430 msgid "Send Invoice on Payment Confirmation" msgstr "" -#: inc/class-settings.php:1419 +#: inc/class-settings.php:1431 msgid "Enabling this option will attach a PDF invoice (marked paid) with the payment confirmation email. This option does not apply to the Manual Gateway, which sends invoices regardless of this option." msgstr "" -#: inc/class-settings.php:1420 +#: inc/class-settings.php:1432 msgid "The invoice files will be saved on the wp-content/uploads/wu-invoices folder." msgstr "" -#: inc/class-settings.php:1430 +#: inc/class-settings.php:1442 msgid "Invoice Numbering Scheme" msgstr "" -#: inc/class-settings.php:1431 +#: inc/class-settings.php:1443 msgid "What should Ultimate Multisite use as the invoice number?" msgstr "" -#: inc/class-settings.php:1436 +#: inc/class-settings.php:1448 msgid "Payment Reference Code" msgstr "" -#: inc/class-settings.php:1437 +#: inc/class-settings.php:1449 msgid "Sequential Number" msgstr "" -#: inc/class-settings.php:1446 +#: inc/class-settings.php:1458 msgid "Next Invoice Number" msgstr "" -#: inc/class-settings.php:1447 +#: inc/class-settings.php:1459 msgid "This number will be used as the invoice number for the next invoice generated on the system. It is incremented by one every time a new invoice is created. You can change it and save it to reset the invoice sequential number to a specific value." msgstr "" -#: inc/class-settings.php:1461 +#: inc/class-settings.php:1473 msgid "Invoice Number Prefix" msgstr "" -#: inc/class-settings.php:1462 +#: inc/class-settings.php:1474 msgid "INV00" msgstr "" #. translators: %%YEAR%%, %%MONTH%%, and %%DAY%% are placeholders but are replaced before shown to the user but are used as examples. -#: inc/class-settings.php:1464 +#: inc/class-settings.php:1476 #, php-format msgid "Use %%YEAR%%, %%MONTH%%, and %%DAY%% to create a dynamic placeholder. E.g. %%YEAR%%-%%MONTH%%-INV will become %s." msgstr "" -#: inc/class-settings.php:1478 +#: inc/class-settings.php:1490 #: inc/ui/class-jumper.php:209 msgid "Payment Gateways" msgstr "" -#: inc/class-settings.php:1479 +#: inc/class-settings.php:1491 msgid "Activate and configure the installed payment gateways in this section." msgstr "" -#: inc/class-settings.php:1494 -#: inc/class-settings.php:1495 +#: inc/class-settings.php:1506 +#: inc/class-settings.php:1507 #: inc/list-tables/class-broadcast-list-table.php:481 #: inc/list-tables/class-email-list-table.php:40 #: inc/ui/class-jumper.php:211 msgid "Emails" msgstr "" -#: inc/class-settings.php:1510 -#: inc/class-settings.php:1511 +#: inc/class-settings.php:1522 +#: inc/class-settings.php:1523 #: inc/integrations/providers/closte/class-closte-domain-mapping.php:48 #: inc/integrations/providers/cloudflare/class-cloudflare-domain-mapping.php:47 #: inc/integrations/providers/cloudways/class-cloudways-domain-mapping.php:47 @@ -9449,139 +9493,139 @@ msgstr "" msgid "Domain Mapping" msgstr "" -#: inc/class-settings.php:1526 -#: inc/class-settings.php:1527 +#: inc/class-settings.php:1538 +#: inc/class-settings.php:1539 msgid "Single Sign-On" msgstr "" -#: inc/class-settings.php:1552 +#: inc/class-settings.php:1564 msgid "Hosting or Panel Providers" msgstr "" -#: inc/class-settings.php:1553 +#: inc/class-settings.php:1565 msgid "Configure and manage the integration with your Hosting or Panel Provider." msgstr "" -#: inc/class-settings.php:1569 +#: inc/class-settings.php:1581 msgid "Import/Export" msgstr "" -#: inc/class-settings.php:1570 +#: inc/class-settings.php:1582 msgid "Export your settings to a JSON file or import settings from a previously exported file." msgstr "" -#: inc/class-settings.php:1581 -#: inc/class-settings.php:1606 +#: inc/class-settings.php:1593 +#: inc/class-settings.php:1618 msgid "Export Settings" msgstr "" -#: inc/class-settings.php:1582 +#: inc/class-settings.php:1594 msgid "Download all your Ultimate Multisite settings as a JSON file for backup or migration purposes." msgstr "" -#: inc/class-settings.php:1594 +#: inc/class-settings.php:1606 msgid "The exported file will contain all ultimate multisite settings defined on this page. This includes general settings, payment gateway configurations, email settings, domain mapping settings, and all other plugin configurations. It does not include products, sites, domains, customers and other entities." msgstr "" -#: inc/class-settings.php:1625 +#: inc/class-settings.php:1637 msgid "Upload a previously exported JSON file to restore settings." msgstr "" -#: inc/class-settings.php:1638 +#: inc/class-settings.php:1650 msgid "Import and Replace All Settings" msgstr "" -#: inc/class-settings.php:1662 +#: inc/class-settings.php:1674 msgid "Importing settings will replace ALL current settings with the values from the uploaded file. This action cannot be undone. We recommend exporting your current settings as a backup before importing." msgstr "" -#: inc/class-settings.php:1690 +#: inc/class-settings.php:1702 msgid "Miscellaneous" msgstr "" -#: inc/class-settings.php:1691 +#: inc/class-settings.php:1703 msgid "Other options that do not fit anywhere else." msgstr "" -#: inc/class-settings.php:1702 +#: inc/class-settings.php:1714 msgid "Hide UI Tours" msgstr "" -#: inc/class-settings.php:1703 +#: inc/class-settings.php:1715 msgid "The UI tours showed by Ultimate Multisite should permanently hide themselves after being seen but if they persist for whatever reason, toggle this option to force them into their viewed state - which will prevent them from showing up again." msgstr "" -#: inc/class-settings.php:1715 +#: inc/class-settings.php:1727 msgid "Disable \"Hover to Zoom\"" msgstr "" -#: inc/class-settings.php:1716 +#: inc/class-settings.php:1728 msgid "By default, Ultimate Multisite adds a \"hover to zoom\" feature, allowing network admins to see larger version of site screenshots and other images across the UI in full-size when hovering over them. You can disable that feature here. Preview tags like the above are not affected." msgstr "" -#: inc/class-settings.php:1726 +#: inc/class-settings.php:1738 msgid "Logging" msgstr "" -#: inc/class-settings.php:1727 +#: inc/class-settings.php:1739 msgid "Log Ultimate Multisite data. This is useful for debugging purposes." msgstr "" -#: inc/class-settings.php:1736 +#: inc/class-settings.php:1748 msgid "Logging Level" msgstr "" -#: inc/class-settings.php:1737 +#: inc/class-settings.php:1749 msgid "Select the level of logging you want to use." msgstr "" -#: inc/class-settings.php:1741 +#: inc/class-settings.php:1753 msgid "PHP Default" msgstr "" -#: inc/class-settings.php:1743 +#: inc/class-settings.php:1755 msgid "Errors Only" msgstr "" -#: inc/class-settings.php:1744 +#: inc/class-settings.php:1756 msgid "Everything" msgstr "" -#: inc/class-settings.php:1754 +#: inc/class-settings.php:1766 msgid "Change the plugin and wordpress behavior." msgstr "" -#: inc/class-settings.php:1769 +#: inc/class-settings.php:1781 msgid "Run Migration Again" msgstr "" -#: inc/class-settings.php:1771 +#: inc/class-settings.php:1783 msgid "Rerun the Migration Wizard if you experience data-loss after migrate." msgstr "" -#: inc/class-settings.php:1774 +#: inc/class-settings.php:1786 msgid "Important: This process can have unexpected behavior with your current Ultimo models.
We recommend that you create a backup before continue." msgstr "" -#: inc/class-settings.php:1777 +#: inc/class-settings.php:1789 msgid "Migrate" msgstr "" -#: inc/class-settings.php:1800 +#: inc/class-settings.php:1812 msgid "Security Mode" msgstr "" #. Translators: Placeholder adds the security mode key and current site url with query string -#: inc/class-settings.php:1802 +#: inc/class-settings.php:1814 #, php-format msgid "Only Ultimate Multisite and other must-use plugins will run on your WordPress install while this option is enabled.
Important: Copy the following URL to disable security mode if something goes wrong and this page becomes unavailable:%2$s
" msgstr "" -#: inc/class-settings.php:1813 +#: inc/class-settings.php:1825 msgid "Remove Data on Uninstall" msgstr "" -#: inc/class-settings.php:1814 +#: inc/class-settings.php:1826 msgid "Remove all saved data for Ultimate Multisite when the plugin is uninstalled." msgstr "" @@ -15185,13 +15229,13 @@ msgid "We were not able to find a user with the given user_id." msgstr "" #. translators: %s: date. -#: inc/functions/date.php:148 +#: inc/functions/date.php:152 #, php-format msgid "on %s" msgstr "" #. translators: %s is a relative past date. -#: inc/functions/date.php:156 +#: inc/functions/date.php:160 #: inc/list-tables/class-base-list-table.php:851 #: views/admin-pages/fields/field-text-display.php:43 #: views/admin-pages/fields/field-text-edit.php:46 @@ -15281,127 +15325,169 @@ msgstr "" msgid "The current payment integration will be updated." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:233 +#: inc/gateways/class-base-stripe-gateway.php:425 +msgid "Could not reach the Stripe Connect service. Please check that your server can make outbound HTTPS requests and try again." +msgstr "" + +#: inc/gateways/class-base-stripe-gateway.php:433 +msgid "Unable to start the Stripe Connect authorization. Please try again or use direct API keys instead." +msgstr "" + +#: inc/gateways/class-base-stripe-gateway.php:555 +msgid "Could not reach the Stripe Connect service to complete authorization. Please check your server's outbound connectivity and try again." +msgstr "" + +#: inc/gateways/class-base-stripe-gateway.php:564 +msgid "Stripe Connect authorization was not accepted. The link may have expired — please try connecting again." +msgstr "" + +#: inc/gateways/class-base-stripe-gateway.php:572 +msgid "Received an unexpected response while completing Stripe Connect. Please try again or use direct API keys instead." +msgstr "" + +#: inc/gateways/class-base-stripe-gateway.php:731 msgid "Change Payment Method" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:346 +#: inc/gateways/class-base-stripe-gateway.php:844 msgid "Manage your membership payment methods." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:379 +#: inc/gateways/class-base-stripe-gateway.php:877 #: inc/gateways/class-stripe-checkout-gateway.php:67 -#: inc/gateways/class-stripe-gateway.php:94 +#: inc/gateways/class-stripe-gateway.php:97 msgid "Credit Card" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:396 +#: inc/gateways/class-base-stripe-gateway.php:894 msgid "Use Stripe Billing Portal" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:542 -#: inc/gateways/class-base-stripe-gateway.php:566 +#: inc/gateways/class-base-stripe-gateway.php:1040 +#: inc/gateways/class-base-stripe-gateway.php:1064 msgid "Invalid API Key provided" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:670 +#: inc/gateways/class-base-stripe-gateway.php:1168 #, php-format msgid "Failed to add stripe webhook: %1$s, %2$s" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:690 +#: inc/gateways/class-base-stripe-gateway.php:1188 #: inc/gateways/class-paypal-gateway.php:345 msgid "Error: No gateway subscription ID found for this membership." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:774 +#: inc/gateways/class-base-stripe-gateway.php:1272 msgid "Amount adjustment based on custom deal." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:1058 +#: inc/gateways/class-base-stripe-gateway.php:1556 msgid "Invalid payment method" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:1264 +#: inc/gateways/class-base-stripe-gateway.php:1762 #: inc/gateways/class-paypal-gateway.php:585 #: inc/gateways/class-paypal-gateway.php:590 msgid "Account credit and other discounts" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:1733 +#: inc/gateways/class-base-stripe-gateway.php:2263 #: inc/gateways/class-paypal-gateway.php:721 msgid "Gateway payment ID not found. Cannot process refund automatically." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:1752 +#: inc/gateways/class-base-stripe-gateway.php:2282 msgid "Gateway payment ID not valid." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:1885 +#: inc/gateways/class-base-stripe-gateway.php:2426 msgid "An unknown error has occurred." msgstr "" #. translators: 1 is the error code and 2 the message. -#: inc/gateways/class-base-stripe-gateway.php:1908 +#: inc/gateways/class-base-stripe-gateway.php:2449 #, php-format msgid "An error has occurred (code: %1$s; message: %2$s)." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:1950 +#: inc/gateways/class-base-stripe-gateway.php:2491 msgid "Event ID not found." msgstr "" #. translators: %s is the customer ID. -#: inc/gateways/class-base-stripe-gateway.php:2053 +#: inc/gateways/class-base-stripe-gateway.php:2594 #, php-format msgid "Exiting Stripe webhook - This call must be handled by %s webhook" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2340 +#: inc/gateways/class-base-stripe-gateway.php:2881 msgid "Duplicate payment." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2353 +#: inc/gateways/class-base-stripe-gateway.php:2894 msgid "Payment not found on refund webhook call." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2359 +#: inc/gateways/class-base-stripe-gateway.php:2900 msgid "Payment is not refundable." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2410 +#: inc/gateways/class-base-stripe-gateway.php:2951 msgid "Membership cancelled via Stripe webhook." msgstr "" #. translators: 1 is the card brand (e.g. VISA), and 2 is the last 4 digits. -#: inc/gateways/class-base-stripe-gateway.php:2439 +#: inc/gateways/class-base-stripe-gateway.php:2980 #, php-format msgid "%1$s ending in %2$s" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2469 +#: inc/gateways/class-base-stripe-gateway.php:3010 msgid "Name on Card" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2615 +#: inc/gateways/class-base-stripe-gateway.php:3157 msgid "Missing plan name or price." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2636 +#: inc/gateways/class-base-stripe-gateway.php:3178 msgid "Empty plan ID." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2717 +#: inc/gateways/class-base-stripe-gateway.php:3259 msgid "Missing product name." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2729 +#: inc/gateways/class-base-stripe-gateway.php:3271 msgid "Empty product ID." msgstr "" +#: inc/gateways/class-base-stripe-gateway.php:3554 +msgid "Payment already completed." +msgstr "" + +#: inc/gateways/class-base-stripe-gateway.php:3563 +msgid "Payment is not in pending status." +msgstr "" + +#: inc/gateways/class-base-stripe-gateway.php:3574 +msgid "No Stripe payment intent found for this payment." +msgstr "" + +#. translators: %s is the intent status from Stripe. +#: inc/gateways/class-base-stripe-gateway.php:3597 +#, php-format +msgid "Payment intent status is: %s" +msgstr "" + +#: inc/gateways/class-base-stripe-gateway.php:3625 +msgid "Payment verified and completed successfully." +msgstr "" + #: inc/gateways/class-manual-gateway.php:90 #: inc/list-tables/class-payment-list-table.php:238 -#: inc/managers/class-gateway-manager.php:424 +#: inc/managers/class-gateway-manager.php:440 msgid "Manual" msgstr "" @@ -15438,7 +15524,7 @@ msgid "Instructions for Payment" msgstr "" #: inc/gateways/class-paypal-gateway.php:193 -#: inc/managers/class-gateway-manager.php:418 +#: inc/managers/class-gateway-manager.php:434 msgid "PayPal" msgstr "" @@ -15646,9 +15732,9 @@ msgstr "" #: inc/gateways/class-paypal-gateway.php:1390 #: inc/gateways/class-paypal-gateway.php:1543 -#: inc/managers/class-gateway-manager.php:283 -#: inc/managers/class-gateway-manager.php:312 -#: inc/managers/class-gateway-manager.php:322 +#: inc/managers/class-gateway-manager.php:299 +#: inc/managers/class-gateway-manager.php:328 +#: inc/managers/class-gateway-manager.php:338 #: views/ui/jumper.php:24 msgid "Error" msgstr "" @@ -15664,7 +15750,7 @@ msgid "An unexpected PayPal error occurred. Error message: %s." msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:50 -#: inc/managers/class-gateway-manager.php:412 +#: inc/managers/class-gateway-manager.php:428 msgid "Stripe Checkout" msgstr "" @@ -15673,12 +15759,12 @@ msgid "Use the settings section below to configure Stripe Checkout as a payment msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:64 -#: inc/gateways/class-stripe-gateway.php:91 +#: inc/gateways/class-stripe-gateway.php:94 msgid "Stripe Public Name" msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:65 -#: inc/gateways/class-stripe-gateway.php:92 +#: inc/gateways/class-stripe-gateway.php:95 msgid "The name to display on the payment method selection field. By default, \"Credit Card\" is used." msgstr "" @@ -15687,126 +15773,189 @@ msgid "Stripe Checkout Sandbox Mode" msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:79 -#: inc/gateways/class-stripe-gateway.php:106 +#: inc/gateways/class-stripe-gateway.php:109 msgid "Toggle this to put Stripe on sandbox mode. This is useful for testing and making sure Stripe is correctly setup to handle your payments." msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:97 -#: inc/gateways/class-stripe-gateway.php:124 +#: inc/gateways/class-stripe-gateway.php:175 msgid "Stripe Test Publishable Key" msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:99 #: inc/gateways/class-stripe-checkout-gateway.php:119 -#: inc/gateways/class-stripe-gateway.php:126 -#: inc/gateways/class-stripe-gateway.php:146 +#: inc/gateways/class-stripe-gateway.php:177 +#: inc/gateways/class-stripe-gateway.php:198 msgid "Make sure you are placing the TEST keys, not the live ones." msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:100 -#: inc/gateways/class-stripe-gateway.php:127 +#: inc/gateways/class-stripe-gateway.php:178 msgid "pk_test_***********" msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:117 -#: inc/gateways/class-stripe-gateway.php:144 +#: inc/gateways/class-stripe-gateway.php:196 msgid "Stripe Test Secret Key" msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:120 -#: inc/gateways/class-stripe-gateway.php:147 +#: inc/gateways/class-stripe-gateway.php:199 msgid "sk_test_***********" msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:137 -#: inc/gateways/class-stripe-gateway.php:164 +#: inc/gateways/class-stripe-gateway.php:217 msgid "Stripe Live Publishable Key" msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:139 #: inc/gateways/class-stripe-checkout-gateway.php:159 -#: inc/gateways/class-stripe-gateway.php:166 -#: inc/gateways/class-stripe-gateway.php:186 +#: inc/gateways/class-stripe-gateway.php:219 +#: inc/gateways/class-stripe-gateway.php:240 msgid "Make sure you are placing the LIVE keys, not the test ones." msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:140 -#: inc/gateways/class-stripe-gateway.php:167 +#: inc/gateways/class-stripe-gateway.php:220 msgid "pk_live_***********" msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:157 -#: inc/gateways/class-stripe-gateway.php:184 +#: inc/gateways/class-stripe-gateway.php:238 msgid "Stripe Live Secret Key" msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:160 -#: inc/gateways/class-stripe-gateway.php:187 +#: inc/gateways/class-stripe-gateway.php:241 msgid "sk_live_***********" msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:171 -#: inc/gateways/class-stripe-gateway.php:198 +#: inc/gateways/class-stripe-gateway.php:253 msgid "Whenever you change your Stripe settings, Ultimate Multisite will automatically check the webhook URLs on your Stripe account to make sure we get notified about changes in subscriptions and payments." msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:177 -#: inc/gateways/class-stripe-gateway.php:204 +#: inc/gateways/class-stripe-gateway.php:259 msgid "Webhook Listener URL" msgstr "" #: inc/gateways/class-stripe-checkout-gateway.php:179 -#: inc/gateways/class-stripe-gateway.php:206 +#: inc/gateways/class-stripe-gateway.php:261 msgid "This is the URL Stripe should send webhook calls to." msgstr "" -#: inc/gateways/class-stripe-checkout-gateway.php:400 +#: inc/gateways/class-stripe-checkout-gateway.php:406 msgid "You will be redirected to a checkout to complete the purchase." msgstr "" -#: inc/gateways/class-stripe-checkout-gateway.php:422 -#: inc/gateways/class-stripe-gateway.php:725 +#: inc/gateways/class-stripe-checkout-gateway.php:428 +#: inc/gateways/class-stripe-gateway.php:778 msgid "Saved Cards" msgstr "" -#: inc/gateways/class-stripe-gateway.php:77 +#: inc/gateways/class-stripe-gateway.php:80 #: inc/list-tables/class-payment-list-table.php:240 -#: inc/managers/class-gateway-manager.php:406 msgid "Stripe" msgstr "" -#: inc/gateways/class-stripe-gateway.php:78 +#: inc/gateways/class-stripe-gateway.php:81 msgid "Use the settings section below to configure Stripe as a payment method." msgstr "" -#: inc/gateways/class-stripe-gateway.php:105 +#: inc/gateways/class-stripe-gateway.php:108 msgid "Stripe Sandbox Mode" msgstr "" +#: inc/gateways/class-stripe-gateway.php:126 +msgid "Stripe Authentication" +msgstr "" + +#: inc/gateways/class-stripe-gateway.php:127 +msgid "Choose how to authenticate with Stripe. OAuth is recommended for easier setup and platform fees." +msgstr "" + +#: inc/gateways/class-stripe-gateway.php:141 +msgid "Stripe Connect (Recommended)" +msgstr "" + +#: inc/gateways/class-stripe-gateway.php:142 +msgid "Connect your Stripe account securely with one click. This provides easier setup and automatic configuration." +msgstr "" + +#: inc/gateways/class-stripe-gateway.php:156 +msgid "Use Direct API Keys (Advanced)" +msgstr "" + +#: inc/gateways/class-stripe-gateway.php:157 +msgid "Toggle to manually enter API keys instead of using OAuth. Use this for backwards compatibility or advanced configurations." +msgstr "" + #. translators: %s is the error message. -#: inc/gateways/class-stripe-gateway.php:261 +#: inc/gateways/class-stripe-gateway.php:316 #, php-format msgid "Error creating Stripe customer: %s" msgstr "" -#: inc/gateways/class-stripe-gateway.php:515 +#: inc/gateways/class-stripe-gateway.php:570 msgid "Missing Stripe payment intent, please try again or contact support if the issue persists." msgstr "" #. translators: first is the customer id, then the customer email. -#: inc/gateways/class-stripe-gateway.php:550 +#: inc/gateways/class-stripe-gateway.php:605 #, php-format msgid "Customer ID: %1$d - User Email: %2$s" msgstr "" -#: inc/gateways/class-stripe-gateway.php:655 +#: inc/gateways/class-stripe-gateway.php:711 msgid "Add new card" msgstr "" -#: inc/gateways/class-stripe-gateway.php:660 +#: inc/gateways/class-stripe-gateway.php:716 msgid "Saved Payment Methods" msgstr "" +#: inc/gateways/class-stripe-gateway.php:878 +msgid "Connected via Stripe Connect" +msgstr "" + +#: inc/gateways/class-stripe-gateway.php:879 +msgid "Account ID:" +msgstr "" + +#. translators: %1$s: the current user display name, %2$s: their password. +#: inc/gateways/class-stripe-gateway.php:882 +#: views/base/addons.php:69 +msgid "Disconnect" +msgstr "" + +#: inc/gateways/class-stripe-gateway.php:904 +msgid "Connect your Stripe account with one click." +msgstr "" + +#: inc/gateways/class-stripe-gateway.php:906 +msgid "Connect with Stripe" +msgstr "" + +#: inc/gateways/class-stripe-gateway.php:907 +msgid "You will be redirected to Stripe to securely authorize the connection." +msgstr "" + +#. translators: %s: the fee percentage +#: inc/gateways/class-stripe-gateway.php:920 +#, php-format +msgid "There is a %s%% fee per-transaction to use the Stripe integration included in the free Ultimate Multisite plugin." +msgstr "" + +#: inc/gateways/class-stripe-gateway.php:925 +msgid "Remove this fee by purchasing any addon and connecting your store." +msgstr "" + +#: inc/gateways/class-stripe-gateway.php:930 +msgid "No application fee — thank you for your support!" +msgstr "" + #. translators: %s is the API URL. #: inc/helpers/class-screenshot.php:61 #, php-format @@ -15820,14 +15969,18 @@ msgstr "" #: inc/helpers/class-screenshot.php:125 #: inc/installers/class-core-installer.php:71 #: inc/installers/class-core-installer.php:81 -#: inc/installers/class-default-content-installer.php:154 -#: inc/installers/class-default-content-installer.php:165 -#: inc/installers/class-default-content-installer.php:176 -#: inc/installers/class-default-content-installer.php:187 -#: inc/installers/class-default-content-installer.php:198 +#: inc/installers/class-default-content-installer.php:155 +#: inc/installers/class-default-content-installer.php:166 +#: inc/installers/class-default-content-installer.php:177 +#: inc/installers/class-default-content-installer.php:188 +#: inc/installers/class-default-content-installer.php:199 #: inc/installers/class-migrator.php:268 #: inc/installers/class-migrator.php:281 #: inc/installers/class-migrator.php:386 +#: inc/installers/class-multisite-network-installer.php:54 +#: inc/installers/class-multisite-network-installer.php:63 +#: inc/installers/class-multisite-network-installer.php:72 +#: inc/installers/class-multisite-network-installer.php:81 msgid "Success!" msgstr "" @@ -15956,12 +16109,12 @@ msgstr "" #. translators: %s is the file name. #: inc/helpers/class-wp-config.php:40 -#: inc/helpers/class-wp-config.php:191 +#: inc/helpers/class-wp-config.php:199 #, php-format msgid "The file %s is not writable" msgstr "" -#: inc/helpers/class-wp-config.php:55 +#: inc/helpers/class-wp-config.php:63 msgid "Ultimate Multisite can't recognize your wp-config.php, please revert it to original state for further process." msgstr "" @@ -16041,98 +16194,98 @@ msgstr "" msgid "Installation of the table %s failed" msgstr "" -#: inc/installers/class-default-content-installer.php:150 +#: inc/installers/class-default-content-installer.php:151 msgid "Create Example Template Site" msgstr "" -#: inc/installers/class-default-content-installer.php:151 +#: inc/installers/class-default-content-installer.php:152 msgid "This will create a template site on your network that you can use as a starting point." msgstr "" -#: inc/installers/class-default-content-installer.php:153 +#: inc/installers/class-default-content-installer.php:154 msgid "Creating Template Site..." msgstr "" -#: inc/installers/class-default-content-installer.php:161 +#: inc/installers/class-default-content-installer.php:162 msgid "Create Example Products" msgstr "" -#: inc/installers/class-default-content-installer.php:162 +#: inc/installers/class-default-content-installer.php:163 msgid "This action will create example products (plans, packages, and services), so you have an starting point." msgstr "" -#: inc/installers/class-default-content-installer.php:164 +#: inc/installers/class-default-content-installer.php:165 msgid "Creating Products..." msgstr "" -#: inc/installers/class-default-content-installer.php:172 +#: inc/installers/class-default-content-installer.php:173 msgid "Create a Checkout Form" msgstr "" -#: inc/installers/class-default-content-installer.php:173 +#: inc/installers/class-default-content-installer.php:174 msgid "This action will create a single-step checkout form that your customers will use to place purchases, as well as the page that goes with it." msgstr "" -#: inc/installers/class-default-content-installer.php:175 +#: inc/installers/class-default-content-installer.php:176 msgid "Creating Checkout Form and Registration Page..." msgstr "" -#: inc/installers/class-default-content-installer.php:183 +#: inc/installers/class-default-content-installer.php:184 msgid "Create the System Emails" msgstr "" -#: inc/installers/class-default-content-installer.php:184 +#: inc/installers/class-default-content-installer.php:185 msgid "This action will create all emails sent by Ultimate Multisite." msgstr "" -#: inc/installers/class-default-content-installer.php:186 +#: inc/installers/class-default-content-installer.php:187 msgid "Creating System Emails..." msgstr "" -#: inc/installers/class-default-content-installer.php:194 +#: inc/installers/class-default-content-installer.php:195 msgid "Create Custom Login Page" msgstr "" -#: inc/installers/class-default-content-installer.php:195 +#: inc/installers/class-default-content-installer.php:196 msgid "This action will create a custom login page and replace the default one." msgstr "" -#: inc/installers/class-default-content-installer.php:197 +#: inc/installers/class-default-content-installer.php:198 msgid "Creating Custom Login Page..." msgstr "" -#: inc/installers/class-default-content-installer.php:233 +#: inc/installers/class-default-content-installer.php:234 msgid "Template Site was not created. Maybe a site with the /template path already exists?" msgstr "" -#: inc/installers/class-default-content-installer.php:265 +#: inc/installers/class-default-content-installer.php:266 msgid "This is an example of a free plan." msgstr "" -#: inc/installers/class-default-content-installer.php:285 +#: inc/installers/class-default-content-installer.php:286 msgid "This is an example of a paid plan." msgstr "" -#: inc/installers/class-default-content-installer.php:304 +#: inc/installers/class-default-content-installer.php:305 msgid "SEO Consulting" msgstr "" -#: inc/installers/class-default-content-installer.php:305 +#: inc/installers/class-default-content-installer.php:306 msgid "This is an example of a service that you can create and charge customers for." msgstr "" -#: inc/installers/class-default-content-installer.php:343 +#: inc/installers/class-default-content-installer.php:344 msgid "Registration Form" msgstr "" -#: inc/installers/class-default-content-installer.php:369 +#: inc/installers/class-default-content-installer.php:370 msgid "Register" msgstr "" -#: inc/installers/class-default-content-installer.php:416 -#: inc/installers/class-migrator.php:2379 +#: inc/installers/class-default-content-installer.php:423 +#: inc/installers/class-migrator.php:2384 #: inc/ui/class-login-form-element.php:156 -#: inc/ui/class-login-form-element.php:377 +#: inc/ui/class-login-form-element.php:380 msgid "Login" msgstr "" @@ -16258,7 +16411,7 @@ msgid "Migrating..." msgstr "" #. Translators: %s is the name of the installer. -#: inc/installers/class-migrator.php:581 +#: inc/installers/class-migrator.php:583 #, php-format msgid "Critical error found when migrating \"%s\"." msgstr "" @@ -16271,6 +16424,64 @@ msgstr "" msgid "Signup" msgstr "" +#: inc/installers/class-multisite-network-installer.php:50 +msgid "Enable Multisite" +msgstr "" + +#: inc/installers/class-multisite-network-installer.php:51 +msgid "Adds WP_ALLOW_MULTISITE constant to wp-config.php." +msgstr "" + +#: inc/installers/class-multisite-network-installer.php:53 +msgid "Enabling multisite..." +msgstr "" + +#: inc/installers/class-multisite-network-installer.php:59 +msgid "Create Network" +msgstr "" + +#: inc/installers/class-multisite-network-installer.php:60 +msgid "Creates network database tables and populates network data." +msgstr "" + +#: inc/installers/class-multisite-network-installer.php:62 +msgid "Creating network tables..." +msgstr "" + +#: inc/installers/class-multisite-network-installer.php:68 +msgid "Update Configuration" +msgstr "" + +#: inc/installers/class-multisite-network-installer.php:69 +msgid "Adds final multisite constants to wp-config.php." +msgstr "" + +#: inc/installers/class-multisite-network-installer.php:71 +msgid "Updating configuration..." +msgstr "" + +#: inc/installers/class-multisite-network-installer.php:77 +msgid "Network Activate Plugin" +msgstr "" + +#: inc/installers/class-multisite-network-installer.php:78 +msgid "Network-activates Ultimate Multisite so it runs across the entire network." +msgstr "" + +#: inc/installers/class-multisite-network-installer.php:80 +msgid "Activating plugin..." +msgstr "" + +#: inc/installers/class-multisite-network-installer.php:119 +msgid "Network configuration not found. Please go back and submit the configuration form again." +msgstr "" + +#. translators: %s full error message. +#: inc/installers/class-multisite-network-installer.php:266 +#, php-format +msgid "Failed to network-activate Ultimate Multisite: %s" +msgstr "" + #: inc/installers/class-recommended-plugins-installer.php:47 msgid "User Switching" msgstr "" @@ -16303,48 +16514,48 @@ msgstr "" msgid "Activated!" msgstr "" -#: inc/installers/class-recommended-plugins-installer.php:210 +#: inc/installers/class-recommended-plugins-installer.php:216 msgid "Unable to resolve plugin download link." msgstr "" -#: inc/installers/class-recommended-plugins-installer.php:226 +#: inc/installers/class-recommended-plugins-installer.php:232 msgid "Installation failed." msgstr "" -#: inc/installers/class-recommended-plugins-installer.php:245 +#: inc/installers/class-recommended-plugins-installer.php:251 msgid "Plugin not found." msgstr "" #. translators: %s is the name of a host provider (e.g. Cloudways, WPMUDev, Closte...). -#: inc/integrations/class-integration-registry.php:357 +#: inc/integrations/class-integration-registry.php:358 #, php-format msgid "%s Integration" msgstr "" -#: inc/integrations/class-integration-registry.php:361 +#: inc/integrations/class-integration-registry.php:362 msgid "Go to the setup wizard to setup this integration." msgstr "" #. translators: %1$s will be replaced with the integration title. E.g. RunCloud -#: inc/integrations/class-integration-registry.php:393 +#: inc/integrations/class-integration-registry.php:394 #, php-format msgid "It looks like you are using %1$s as your hosting provider, yet the %1$s integration module is not active. In order for the domain mapping integration to work with %1$s, you might want to activate that module." msgstr "" #. translators: %s is the integration name. -#: inc/integrations/class-integration-registry.php:398 +#: inc/integrations/class-integration-registry.php:399 #, php-format msgid "Activate %s" msgstr "" #. translators: %s is the integration name. -#: inc/integrations/class-integration-registry.php:411 +#: inc/integrations/class-integration-registry.php:412 #, php-format msgid "The %s integration module is active but not properly configured. Please complete the setup." msgstr "" #. translators: %s is the integration name. -#: inc/integrations/class-integration-registry.php:416 +#: inc/integrations/class-integration-registry.php:417 #, php-format msgid "Setup %s" msgstr "" @@ -16955,7 +17166,7 @@ msgstr "" msgid "WPMU DEV is one of the largest companies in the WordPress space. Founded in 2004, it was one of the first companies to scale the Website as a Service model with products such as Edublogs and CampusPress." msgstr "" -#: inc/internal/class-memory-trap.php:100 +#: inc/internal/class-memory-trap.php:99 msgid "Your server's PHP and WordPress memory limits are too low to perform this check. You might need to contact your host provider and ask the PHP memory limit in particular to be raised." msgstr "" @@ -18195,87 +18406,87 @@ msgstr "" msgid "No targets found." msgstr "" -#: inc/managers/class-email-manager.php:234 +#: inc/managers/class-email-manager.php:223 msgid "Sender Settings" msgstr "" -#: inc/managers/class-email-manager.php:235 +#: inc/managers/class-email-manager.php:224 msgid "Change the settings of the email headers, like from and name." msgstr "" -#: inc/managers/class-email-manager.php:244 +#: inc/managers/class-email-manager.php:233 msgid "\"From\" Name" msgstr "" -#: inc/managers/class-email-manager.php:245 +#: inc/managers/class-email-manager.php:234 msgid "How the sender name will appear in emails sent by Ultimate Multisite." msgstr "" -#: inc/managers/class-email-manager.php:259 +#: inc/managers/class-email-manager.php:248 msgid "\"From\" E-mail" msgstr "" -#: inc/managers/class-email-manager.php:260 +#: inc/managers/class-email-manager.php:249 msgid "How the sender email will appear in emails sent by Ultimate Multisite." msgstr "" -#: inc/managers/class-email-manager.php:274 +#: inc/managers/class-email-manager.php:263 msgid "Template Settings" msgstr "" -#: inc/managers/class-email-manager.php:275 +#: inc/managers/class-email-manager.php:264 msgid "Change the settings of the email templates." msgstr "" -#: inc/managers/class-email-manager.php:284 +#: inc/managers/class-email-manager.php:273 msgid "Email Templates Style" msgstr "" -#: inc/managers/class-email-manager.php:302 +#: inc/managers/class-email-manager.php:291 msgid "Expiring Notification Settings" msgstr "" -#: inc/managers/class-email-manager.php:303 +#: inc/managers/class-email-manager.php:292 msgid "Change the settings for the expiring notification (trials and subscriptions) emails." msgstr "" -#: inc/managers/class-email-manager.php:312 +#: inc/managers/class-email-manager.php:301 msgid "Days to Expire" msgstr "" -#: inc/managers/class-email-manager.php:313 +#: inc/managers/class-email-manager.php:302 msgid "Select when we should send the notification email. If you select 3 days, for example, a notification email will be sent to every membership (or trial period) expiring in the next 3 days. Memberships are checked hourly." msgstr "" -#: inc/managers/class-email-manager.php:315 +#: inc/managers/class-email-manager.php:304 msgid "e.g. 3" msgstr "" -#: inc/managers/class-email-manager.php:412 +#: inc/managers/class-email-manager.php:401 msgid "You got a new payment!" msgstr "" -#: inc/managers/class-email-manager.php:425 +#: inc/managers/class-email-manager.php:414 msgid "We got your payment!" msgstr "" -#: inc/managers/class-email-manager.php:438 +#: inc/managers/class-email-manager.php:427 msgid "A new site was created on your Network!" msgstr "" -#: inc/managers/class-email-manager.php:451 +#: inc/managers/class-email-manager.php:440 msgid "Your site is ready!" msgstr "" -#: inc/managers/class-email-manager.php:464 +#: inc/managers/class-email-manager.php:453 msgid "Confirm your email address!" msgstr "" -#: inc/managers/class-email-manager.php:477 +#: inc/managers/class-email-manager.php:466 msgid "A new domain was added to your Network!" msgstr "" -#: inc/managers/class-email-manager.php:490 +#: inc/managers/class-email-manager.php:479 msgid "You have a new pending payment!" msgstr "" @@ -18370,34 +18581,54 @@ msgstr "" msgid "Review this action carefully." msgstr "" -#: inc/managers/class-gateway-manager.php:282 +#: inc/managers/class-gateway-manager.php:298 msgid "Missing gateway parameter." msgstr "" -#: inc/managers/class-gateway-manager.php:361 +#: inc/managers/class-gateway-manager.php:377 msgid "Active Payment Gateways" msgstr "" -#: inc/managers/class-gateway-manager.php:362 +#: inc/managers/class-gateway-manager.php:378 msgid "Payment gateways are what your customers will use to pay." msgstr "" -#: inc/managers/class-gateway-manager.php:405 -msgid "Stripe is a suite of payment APIs that powers commerce for businesses of all sizes, including subscription management." +#: inc/managers/class-gateway-manager.php:421 +msgid "Accept payments in hundreds of currencies with many express checkout methods or local payment methods." +msgstr "" + +#: inc/managers/class-gateway-manager.php:422 +msgid "Stripe (Recommended)" msgstr "" -#: inc/managers/class-gateway-manager.php:411 -msgid "Stripe Checkout is the hosted solution for checkouts using Stripe." +#: inc/managers/class-gateway-manager.php:427 +msgid "Redirect to collect payment information on Stripe's Checkout page." msgstr "" -#: inc/managers/class-gateway-manager.php:417 +#: inc/managers/class-gateway-manager.php:433 msgid "PayPal is the leading provider in checkout solutions and it is the easier way to get your network subscriptions going." msgstr "" -#: inc/managers/class-gateway-manager.php:423 +#: inc/managers/class-gateway-manager.php:439 msgid "Use the Manual Gateway to allow users to pay you directly via bank transfers, checks, or other channels." msgstr "" +#: inc/managers/class-gateway-manager.php:601 +msgid "Payment hash is required." +msgstr "" + +#: inc/managers/class-gateway-manager.php:615 +msgid "Payment completed." +msgstr "" + +#: inc/managers/class-gateway-manager.php:633 +msgid "Non-Stripe payment, cannot verify." +msgstr "" + +#: inc/managers/class-gateway-manager.php:645 +msgid "Gateway does not support verification." +msgstr "" + #: inc/managers/class-limitation-manager.php:145 #: inc/managers/class-limitation-manager.php:551 #: inc/managers/class-limitation-manager.php:557 @@ -18668,8 +18899,8 @@ msgstr "" #: inc/managers/class-membership-manager.php:204 #: inc/managers/class-membership-manager.php:316 #: inc/managers/class-membership-manager.php:387 -#: inc/managers/class-payment-manager.php:336 -#: inc/managers/class-payment-manager.php:381 +#: inc/managers/class-payment-manager.php:344 +#: inc/managers/class-payment-manager.php:389 #: inc/ui/class-site-actions-element.php:621 #: inc/ui/class-site-actions-element.php:1112 #: inc/ui/class-site-actions-element.php:1304 @@ -18753,7 +18984,7 @@ msgstr "" msgid "Pay Now" msgstr "" -#: inc/managers/class-payment-manager.php:304 +#: inc/managers/class-payment-manager.php:297 msgid "This invoice does not exist." msgstr "" @@ -18988,20 +19219,20 @@ msgstr[1] "" msgid "%1$s one time payment" msgstr "" -#: inc/models/class-payment.php:961 +#: inc/models/class-payment.php:960 msgid "(provisional)" msgstr "" -#: inc/models/class-payment.php:1081 +#: inc/models/class-payment.php:1080 msgid "Full Refund" msgstr "" -#: inc/models/class-payment.php:1085 +#: inc/models/class-payment.php:1084 msgid "Partial Refund" msgstr "" #. translators: %s is the date of processing. -#: inc/models/class-payment.php:1095 +#: inc/models/class-payment.php:1094 #, php-format msgid "Processed on %s" msgstr "" @@ -19915,7 +20146,7 @@ msgid "Remember Me Description" msgstr "" #: inc/ui/class-login-form-element.php:271 -#: inc/ui/class-login-form-element.php:393 +#: inc/ui/class-login-form-element.php:396 msgid "Keep me logged in for two weeks." msgstr "" @@ -20355,31 +20586,33 @@ msgstr "" msgid "Template Selector Layout" msgstr "" -#: inc/ui/class-template-switching-element.php:172 -msgid "Want to add customized template selection templates?
See how you can do that here." +#. translators: %s the doc url +#: inc/ui/class-template-switching-element.php:173 +#, php-format +msgid "Want to add customized template selection templates?
See how you can do that here." msgstr "" -#: inc/ui/class-template-switching-element.php:279 +#: inc/ui/class-template-switching-element.php:280 msgid "You are not allowed to use this template." msgstr "" -#: inc/ui/class-template-switching-element.php:283 +#: inc/ui/class-template-switching-element.php:284 msgid "You need to provide a valid template to duplicate." msgstr "" -#: inc/ui/class-template-switching-element.php:387 +#: inc/ui/class-template-switching-element.php:388 msgid "← Back to Template Selection" msgstr "" -#: inc/ui/class-template-switching-element.php:397 +#: inc/ui/class-template-switching-element.php:398 msgid "Confirm template switch?" msgstr "" -#: inc/ui/class-template-switching-element.php:398 +#: inc/ui/class-template-switching-element.php:399 msgid "Switching your current template completely overwrites the content of your site with the contents of the newly chosen template. All customizations will be lost. This action cannot be undone." msgstr "" -#: inc/ui/class-template-switching-element.php:412 +#: inc/ui/class-template-switching-element.php:413 msgid "Process Switch" msgstr "" @@ -20499,11 +20732,6 @@ msgstr "" msgid "Disconnect your site" msgstr "" -#. translators: %1$s: the current user display name, %2$s: their password. -#: views/base/addons.php:69 -msgid "Disconnect" -msgstr "" - #: views/base/addons.php:73 msgid "Ultimate Multisite might be at risk because it’s unable to automatically update add-ons. Please complete the connection to get updates and streamlined support." msgstr "" @@ -20873,11 +21101,11 @@ msgstr "" msgid "See Main Site" msgstr "" -#: views/base/wizard/submit-box.php:14 +#: views/base/wizard/submit-box.php:16 msgid "← Go Back" msgstr "" -#: views/base/wizard/submit-box.php:20 +#: views/base/wizard/submit-box.php:23 #: views/wizards/host-integrations/activation.php:104 msgid "Continue" msgstr "" @@ -22282,7 +22510,7 @@ msgid "Use Site Tools or contact support for cPanel credentials" msgstr "" #: views/wizards/host-integrations/cpanel-instructions.php:134 -msgid "A2 Hosting, InMotion:" +msgid "Hosting.com, InMotion:" msgstr "" #: views/wizards/host-integrations/cpanel-instructions.php:134 diff --git a/tests/e2e/cypress/integration/000-setup.spec.js b/tests/e2e/cypress/integration/000-setup.spec.js index 1189109b..39aeb5be 100644 --- a/tests/e2e/cypress/integration/000-setup.spec.js +++ b/tests/e2e/cypress/integration/000-setup.spec.js @@ -62,4 +62,12 @@ describe("Ultimate Multisite Setup", () => { expect(result.stdout).to.contain("never"); }); }); + + it("Should reset password strength to default", () => { + cy.wpCli( + "eval \"wu_save_setting('password_strength', 'strong'); echo wu_get_setting('password_strength', 'none');\"" + ).then((result) => { + expect(result.stdout).to.contain("strong"); + }); + }); }); diff --git a/tests/e2e/cypress/integration/010-manual-checkout-flow.spec.js b/tests/e2e/cypress/integration/010-manual-checkout-flow.spec.js index 109b9a07..f81951cf 100644 --- a/tests/e2e/cypress/integration/010-manual-checkout-flow.spec.js +++ b/tests/e2e/cypress/integration/010-manual-checkout-flow.spec.js @@ -3,7 +3,7 @@ describe("Manual Gateway Checkout Flow", () => { const customerData = { username: `manualcust${timestamp}`, email: `manualcust${timestamp}@test.com`, - password: "TestPassword123!", + password: "xK9#mL2$vN5@qR", }; const siteData = { title: "Manual Test Site", @@ -54,20 +54,16 @@ describe("Manual Gateway Checkout Flow", () => { } }); - // Fill billing address if present - cy.get("body").then(($body) => { - if ($body.find("#field-billing_country").length > 0) { - cy.get("#field-billing_country").select("US"); - } else if ($body.find('[name="billing_address[billing_country]"]').length > 0) { - cy.get('[name="billing_address[billing_country]"]').select("US"); - } + // Fill billing address — fields are controlled by v-show on order.should_collect_payment + // After plan selection, create_order AJAX fires and fields become visible + cy.get("#field-billing_country", { timeout: 15000 }) + .should("be.visible") + .select("US"); - if ($body.find("#field-billing_zip_code").length > 0) { - cy.get("#field-billing_zip_code").clear().type("94105"); - } else if ($body.find('[name="billing_address[billing_zip_code]"]').length > 0) { - cy.get('[name="billing_address[billing_zip_code]"]').clear().type("94105"); - } - }); + cy.get("#field-billing_zip_code", { timeout: 15000 }) + .should("be.visible") + .clear() + .type("94105"); // Submit the UM checkout form cy.get( diff --git a/tests/e2e/cypress/integration/020-free-trial-flow.spec.js b/tests/e2e/cypress/integration/020-free-trial-flow.spec.js index d932b1b6..15469828 100644 --- a/tests/e2e/cypress/integration/020-free-trial-flow.spec.js +++ b/tests/e2e/cypress/integration/020-free-trial-flow.spec.js @@ -3,7 +3,7 @@ describe("Free Trial Checkout Flow", () => { const customerData = { username: `trialcust${timestamp}`, email: `trialcust${timestamp}@test.com`, - password: "TestPassword123!", + password: "xK9#mL2$vN5@qR", }; const siteData = { title: "Trial Test Site", diff --git a/tests/e2e/cypress/integration/030-stripe-checkout-flow.spec.js b/tests/e2e/cypress/integration/030-stripe-checkout-flow.spec.js index a1800fa3..85f6cdbb 100644 --- a/tests/e2e/cypress/integration/030-stripe-checkout-flow.spec.js +++ b/tests/e2e/cypress/integration/030-stripe-checkout-flow.spec.js @@ -3,7 +3,7 @@ describe("Stripe Gateway Checkout Flow", () => { const customerData = { username: `stripecust${timestamp}`, email: `stripecust${timestamp}@test.com`, - password: "TestPassword123!", + password: "xK9#mL2$vN5@qR", }; const siteData = { title: "Stripe Test Site", diff --git a/tests/e2e/cypress/support/commands/login.js b/tests/e2e/cypress/support/commands/login.js index ad10841d..efac52f4 100644 --- a/tests/e2e/cypress/support/commands/login.js +++ b/tests/e2e/cypress/support/commands/login.js @@ -28,8 +28,11 @@ Cypress.Commands.add("loginByApi", (username, password) => { Cypress.Commands.add("loginByForm", (username, password) => { cy.session(["loginByForm", username, password], () => { - cy.visit("/wp-admin/"); - cy.location("pathname").should("contain", "/wp-login.php"); + cy.visit("/wp-login.php"); + // Handle both default wp-login.php and custom login page (/login/) + cy.location("pathname").should("satisfy", (path) => { + return path.includes("/wp-login.php") || path.includes("/login"); + }); cy.get("#rememberme").should("be.visible").and("not.be.checked").click(); cy.get("#user_login").should("be.visible").setValue(username); cy.get("#user_pass") @@ -38,6 +41,7 @@ Cypress.Commands.add("loginByForm", (username, password) => { .type("{enter}"); cy.location("pathname") .should("not.contain", "/wp-login.php") + .and("not.contain", "/login") .and("equal", "/wp-admin/"); }); }); From 0c176cffc32cc28060bac738281f1a359fb30759 Mon Sep 17 00:00:00 2001 From: David Stone Date: Sun, 15 Feb 2026 20:41:37 -0700 Subject: [PATCH 03/10] Fix typos, billing_country validation, and sodium decryption error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix "scarry" → "scary" and "specially" → "especially" in setup wizard - Use v-if instead of v-show for billing_country so it's removed from the DOM when payment isn't required, preventing required_with validation from firing on a hidden field - Wrap sodium_crypto_secretbox_open in try-catch for SodiumException to preserve the graceful empty-string return contract Co-Authored-By: Claude Opus 4.6 --- inc/admin-pages/class-setup-wizard-admin-page.php | 2 +- .../class-signup-field-billing-address.php | 15 +++++++++++++-- inc/helpers/class-credential-store.php | 10 ++++++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/inc/admin-pages/class-setup-wizard-admin-page.php b/inc/admin-pages/class-setup-wizard-admin-page.php index 4eea8a2a..f8f60986 100644 --- a/inc/admin-pages/class-setup-wizard-admin-page.php +++ b/inc/admin-pages/class-setup-wizard-admin-page.php @@ -462,7 +462,7 @@ public function get_sections() { $sections['defaults'] = [ 'title' => __('Default Content', 'ultimate-multisite'), - 'description' => __('Starting from scratch can be scarry, specially when first starting out. In this step, you can create default content to have a starting point for your network. Everything can be customized later.', 'ultimate-multisite'), + 'description' => __('Starting from scratch can be scary, especially when first starting out. In this step, you can create default content to have a starting point for your network. Everything can be customized later.', 'ultimate-multisite'), 'next_label' => Default_Content_Installer::get_instance()->all_done() ? __('Go to the Next Step →', 'ultimate-multisite') : __('Install', 'ultimate-multisite'), 'disable_next' => true, 'fields' => [ diff --git a/inc/checkout/signup-fields/class-signup-field-billing-address.php b/inc/checkout/signup-fields/class-signup-field-billing-address.php index 3bd4ff5e..95a91e63 100644 --- a/inc/checkout/signup-fields/class-signup-field-billing-address.php +++ b/inc/checkout/signup-fields/class-signup-field-billing-address.php @@ -266,11 +266,22 @@ public function to_fields_array($attributes) { } } - foreach ($fields as &$field) { + foreach ($fields as $field_key => &$field) { $field['wrapper_classes'] = trim(wu_get_isset($field, 'wrapper_classes', '') . ' ' . $attributes['element_classes']); - $field['wrapper_html_attr']['v-show'] = 'order === false || order.should_collect_payment'; $field['wrapper_html_attr']['v-cloak'] = 1; + /* + * billing_country uses v-if so the input is removed from the DOM + * when payment is not required. This prevents the server-side + * required_with:billing_country rule from firing on a field + * the user cannot see or edit. + */ + if ('billing_country' === $field_key) { + $field['wrapper_html_attr']['v-if'] = 'order === false || order.should_collect_payment'; + } else { + $field['wrapper_html_attr']['v-show'] = 'order === false || order.should_collect_payment'; + } + /* * When zip_and_country is enabled, remove the billing address fields * from the DOM when any Stripe gateway is selected. Stripe's Payment diff --git a/inc/helpers/class-credential-store.php b/inc/helpers/class-credential-store.php index bab0cbc1..670d9d26 100644 --- a/inc/helpers/class-credential-store.php +++ b/inc/helpers/class-credential-store.php @@ -167,8 +167,14 @@ private static function decrypt_sodium(string $value): string { return ''; } - $key = self::get_sodium_key(); - $decrypted = sodium_crypto_secretbox_open($encrypted, $nonce, $key); + try { + $key = self::get_sodium_key(); + $decrypted = sodium_crypto_secretbox_open($encrypted, $nonce, $key); + } catch (\SodiumException $e) { + wu_log_add('credential-store', 'Sodium decryption failed: ' . $e->getMessage(), \Psr\Log\LogLevel::ERROR); + + return ''; + } return false === $decrypted ? '' : $decrypted; } From 82bc39f72f99f00768bc1aff4a25d13d9ac3eb4f Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 16 Feb 2026 13:16:56 -0700 Subject: [PATCH 04/10] Fix %2F being stripped from URLs during SSO and domain mapping redirects wp_parse_str() + add_query_arg() decodes percent-encoded values like %2F without re-encoding them, causing parameters inside redirect_to to leak as top-level query params. This broke WooCommerce analytics URLs that use path=%2Fanalytics%2Foverview. In filter_login_url(), preserve the raw query string instead of parsing and rebuilding it. In Primary_Domain redirect, use the full redirect URL directly since replace_url() already handles the complete URL. Co-Authored-By: Claude Opus 4.6 --- inc/checkout/class-checkout-pages.php | 30 ++++++++++----------- inc/domain-mapping/class-primary-domain.php | 23 ++++++++-------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/inc/checkout/class-checkout-pages.php b/inc/checkout/class-checkout-pages.php index db92e56e..a3cb2afa 100644 --- a/inc/checkout/class-checkout-pages.php +++ b/inc/checkout/class-checkout-pages.php @@ -579,7 +579,7 @@ public function redirect_to_registration_page(): void { * @param bool $force_reauth If we need to force reauth. * @return string */ - public function filter_login_url($login_url, $redirect, $force_reauth = false) { + public function filter_login_url($login_url, $redirect, $force_reauth = false) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed /** * Fix incompatibility with UIPress, making sure we only filter after wp_loaded ran. @@ -594,28 +594,26 @@ public function filter_login_url($login_url, $redirect, $force_reauth = false) { return $login_url; } - $params = []; - - $old_url_params = wp_parse_url($login_url, PHP_URL_QUERY); - - wp_parse_str($old_url_params, $params); - $new_login_url = $this->get_page_url('login'); if ( ! $new_login_url) { return $login_url; } - if ($params) { - $new_login_url = add_query_arg($params, $new_login_url); - } - - if ($redirect) { - $new_login_url = add_query_arg('redirect_to', rawurlencode($redirect), $new_login_url); - } + /* + * Preserve the raw query string from the original login URL + * to avoid URL decoding issues. wp_parse_str() + add_query_arg() + * decodes percent-encoded values like %2F without re-encoding them, + * which causes parameters inside redirect_to to leak as top-level + * query params (e.g., path=%2Fanalytics%2Foverview leaking out of + * the redirect_to value). By preserving the raw query string, we + * maintain the original encoding. WordPress's wp_login_url() already + * adds redirect_to and reauth before this filter runs. + */ + $raw_query = wp_parse_url($login_url, PHP_URL_QUERY); - if ($force_reauth) { - $new_login_url = add_query_arg('reauth', 1, $new_login_url); + if ($raw_query) { + $new_login_url .= (str_contains($new_login_url, '?') ? '&' : '?') . $raw_query; } return $new_login_url; diff --git a/inc/domain-mapping/class-primary-domain.php b/inc/domain-mapping/class-primary-domain.php index 9f97d68c..8951fd32 100644 --- a/inc/domain-mapping/class-primary-domain.php +++ b/inc/domain-mapping/class-primary-domain.php @@ -155,26 +155,27 @@ public function maybe_redirect_to_mapped_or_network_domain(): void { $mapped_url_to_compare = $mapped_url['host']; - $query_args = []; - - if (wu_get_isset($current_url, 'query')) { - wp_parse_str($current_url['query'], $query_args); - } - $redirect_url = false; if ('force_map' === $redirect_settings && $current_url_to_compare !== $mapped_url_to_compare) { $redirect_url = Domain_Mapping::get_instance()->replace_url(wu_get_current_url(), $mapped_domain); - - $query_args = array_map(fn($value) => Domain_Mapping::get_instance()->replace_url($value, $mapped_domain), $query_args); } elseif ('force_network' === $redirect_settings && $current_url_to_compare === $mapped_url_to_compare) { $redirect_url = wu_restore_original_url(wu_get_current_url(), $site->get_id()); - - $query_args = array_map(fn($value) => wu_restore_original_url($value, $site->get_id()), $query_args); } if ($redirect_url) { - wp_redirect(add_query_arg($query_args, $redirect_url)); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect + /* + * Use the redirect URL directly instead of parsing and rebuilding + * query args with wp_parse_str() + add_query_arg(). That approach + * URL-decodes percent-encoded values like %2F without re-encoding + * them, breaking URLs that use encoded slashes in query parameters + * (e.g., WooCommerce analytics path=%2Fanalytics%2Foverview). + * + * replace_url() and wu_restore_original_url() already handle the + * full URL including the query string, so separate query arg + * processing is not needed. + */ + wp_redirect($redirect_url); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect exit; } From b232e296141da66e645bd00114b2523c616518c0 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 16 Feb 2026 14:40:11 -0700 Subject: [PATCH 05/10] Add E2E test for SSO cross-domain authentication Tests that SSO correctly triggers when visiting a subsite through a mapped domain (127.0.0.1 vs localhost), verifying domain mapping resolution, SSO redirect initiation, and auto-authentication flow. Co-Authored-By: Claude Opus 4.6 --- tests/e2e/cypress/fixtures/setup-sso-test.php | 121 ++++++++++++++++ .../integration/060-sso-cross-domain.spec.js | 129 ++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 tests/e2e/cypress/fixtures/setup-sso-test.php create mode 100644 tests/e2e/cypress/integration/060-sso-cross-domain.spec.js diff --git a/tests/e2e/cypress/fixtures/setup-sso-test.php b/tests/e2e/cypress/fixtures/setup-sso-test.php new file mode 100644 index 00000000..effc2d8b --- /dev/null +++ b/tests/e2e/cypress/fixtures/setup-sso-test.php @@ -0,0 +1,121 @@ +domain; + +// Extract port from the network domain, if present. +$port = ''; +if (preg_match('/:(\d+)$/', $network_domain, $m)) { + $port = ':' . $m[1]; +} + +$mapped_domain = '127.0.0.1' . $port; + +// 1. Create a subsite for SSO testing (or reuse if it already exists). +$existing = get_blog_id_from_url($network_domain, '/sso-test-site/'); + +if ($existing) { + $site_id = $existing; +} else { + $site_id = wpmu_create_blog( + $network_domain, + '/sso-test-site/', + 'SSO Test Site', + get_current_user_id() + ); + + if (is_wp_error($site_id)) { + echo wp_json_encode([ + 'error' => $site_id->get_error_message(), + ]); + exit(1); + } +} + +// 2. Insert domain mapping for 127.0.0.1:PORT directly into the DB. +// The Domain model's validation rejects IP addresses, so we bypass it. +$table = $wpdb->base_prefix . 'wu_domain_mappings'; +$now = current_time('mysql'); + +// Check if the mapping already exists (look for both with and without port). +$existing_domain = $wpdb->get_var( + $wpdb->prepare( + "SELECT id FROM {$table} WHERE domain IN (%s, %s) AND blog_id = %d LIMIT 1", + $mapped_domain, + '127.0.0.1', + $site_id + ) +); + +if ($existing_domain) { + // Update existing record to ensure domain includes port. + $wpdb->update( + $table, + ['domain' => $mapped_domain, 'active' => 1, 'stage' => 'done'], + ['id' => $existing_domain], + ['%s', '%d', '%s'], + ['%d'] + ); + $domain_id = (int) $existing_domain; +} else { + $inserted = $wpdb->insert( + $table, + [ + 'blog_id' => $site_id, + 'domain' => $mapped_domain, + 'active' => 1, + 'primary_domain' => 1, + 'secure' => 0, + 'stage' => 'done', + 'date_created' => $now, + 'date_modified' => $now, + ], + ['%d', '%s', '%d', '%d', '%d', '%s', '%s', '%s'] + ); + + if (! $inserted) { + echo wp_json_encode([ + 'error' => 'Domain insert failed: ' . $wpdb->last_error, + 'site_id' => $site_id, + ]); + exit(1); + } + + $domain_id = $wpdb->insert_id; +} + +// Also set the wu_dmtable property so get_by_domain() can find it. +if (empty($wpdb->wu_dmtable)) { + $wpdb->wu_dmtable = $table; +} + +// Clear domain mapping cache for this domain. +wp_cache_delete('domain:' . $mapped_domain, 'domain_mappings'); +wp_cache_delete('domain:127.0.0.1', 'domain_mappings'); + +// 3. Enable SSO and disable the loading overlay (avoids flicker in tests). +wu_save_setting('enable_sso', true); +wu_save_setting('enable_sso_loading_overlay', false); + +// 4. Output result for Cypress. +echo wp_json_encode([ + 'site_id' => $site_id, + 'domain' => $mapped_domain, + 'domain_id' => $domain_id, +]); diff --git a/tests/e2e/cypress/integration/060-sso-cross-domain.spec.js b/tests/e2e/cypress/integration/060-sso-cross-domain.spec.js new file mode 100644 index 00000000..bce6e71c --- /dev/null +++ b/tests/e2e/cypress/integration/060-sso-cross-domain.spec.js @@ -0,0 +1,129 @@ +/** + * SSO Cross-Domain Authentication E2E Tests + * + * Verifies that Single Sign-On works: a user logged into the main site + * (localhost:8889) is automatically authenticated when visiting a subsite + * through a mapped domain (127.0.0.1:8889). + * + * Uses localhost vs 127.0.0.1 — two genuinely different hostnames that + * both resolve without DNS/hosts changes, with cookies scoped per hostname. + * + * Environment note: wp-env uses non-standard port 8889. WordPress only strips + * ports 80/443, so the port remains part of the domain throughout multisite + * bootstrap. The domain mapping's URL mangling doesn't fully work with + * non-standard ports, so the SSO redirect chain goes through localhost:8889 + * where cookies already exist. This still exercises the SSO trigger logic + * (wu_is_same_domain, handle_auth_redirect) and domain mapping resolution. + */ +describe("SSO Cross-Domain Authentication", () => { + const mainSiteUrl = "http://localhost:8889"; + const mappedDomainUrl = "http://127.0.0.1:8889"; + const adminUser = "admin"; + const adminPass = "password"; + + before(() => { + // Ensure we start on the main site for login / WP-CLI commands. + cy.visit("/wp-login.php", { failOnStatusCode: false }); + + // Run the SSO setup fixture: creates subsite + domain mapping + enables SSO. + cy.wpCliFile("tests/e2e/cypress/fixtures/setup-sso-test.php", { + failOnNonZeroExit: false, + }).then((result) => { + const output = result.stdout.trim(); + cy.log(`SSO setup output: ${output}`); + + // Verify setup succeeded (output is JSON without an error key). + expect(output).to.contain("site_id"); + expect(output).to.not.contain('"error"'); + }); + }); + + it("Should resolve mapped domain to the correct subsite", () => { + // Verify domain mapping works: 127.0.0.1:8889 should serve the subsite, + // not redirect to the main site homepage. + cy.request({ + url: `${mappedDomainUrl}/`, + followRedirect: false, + failOnStatusCode: false, + }).then((response) => { + // Should get 200 (subsite front page) — NOT a 302 redirect to main site. + expect(response.status).to.eq(200); + }); + }); + + it( + "Should trigger SSO redirect when visiting wp-admin on mapped domain", + { retries: 1 }, + () => { + // Without login cookies for 127.0.0.1, visiting wp-admin should trigger + // the SSO redirect chain (handle_auth_redirect detects different domain). + cy.request({ + url: `${mappedDomainUrl}/wp-admin/`, + followRedirect: false, + failOnStatusCode: false, + }).then((response) => { + // SSO triggers a 302 redirect to wp-login.php?sso=login + expect(response.status).to.eq(302); + expect(response.headers.location).to.include("sso=login"); + expect(response.headers.location).to.include("wp-login.php"); + }); + } + ); + + it( + "Should auto-authenticate on subsite via SSO after main-site login", + { retries: 1 }, + () => { + // 1. Log in on the main site (localhost:8889). + cy.loginByApi(adminUser, adminPass); + + // Verify login worked on main site. + cy.visit("/wp-admin/", { failOnStatusCode: false }); + cy.url().should("include", "/wp-admin/"); + cy.get("body").should("have.class", "wp-admin"); + + // 2. Visit wp-admin on the mapped domain (127.0.0.1:8889). + // SSO triggers: handle_auth_redirect() detects different domain + not + // logged in, redirects to wp-login.php?sso=login. Because this wp-env + // uses port 8889, the redirect goes through localhost:8889 where auth + // cookies exist, so the user is immediately authenticated. + // + // The final landing page is the subsite's wp-admin on localhost:8889. + cy.visit(`${mappedDomainUrl}/wp-admin/`, { + failOnStatusCode: false, + }); + + // 3. After SSO redirect chain completes, the user should land on the + // subsite's wp-admin dashboard (authenticated). + cy.url({ timeout: 60000 }).should("include", "/wp-admin/"); + cy.get("body", { timeout: 30000 }).should("have.class", "wp-admin"); + + // Confirm we are logged in: admin bar should be present. + cy.get("#wpadminbar").should("exist"); + + // Confirm we are on the SSO test subsite (not the main site). + cy.url().should("include", "/sso-test-site/"); + } + ); + + it( + "Should preserve redirect_to parameter through SSO flow", + { retries: 1 }, + () => { + // This verifies that URL parameters survive the SSO redirect chain. + cy.loginByApi(adminUser, adminPass); + + // Visit a specific wp-admin page on the mapped domain. + const targetPath = "/wp-admin/options-general.php"; + + cy.visit(`${mappedDomainUrl}${targetPath}`, { + failOnStatusCode: false, + }); + + // After SSO, the user should land on the requested page (or wp-admin). + cy.url({ timeout: 60000 }).should("include", "/wp-admin/"); + cy.get("body", { timeout: 30000 }).should("have.class", "wp-admin"); + cy.get("#wpadminbar").should("exist"); + } + ); +}); From 9f926e18d793c0f23738e931926f5dbbd8d5497e Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 16 Feb 2026 15:07:55 -0700 Subject: [PATCH 06/10] disable overlay by default --- inc/managers/class-domain-manager.php | 2 +- inc/sso/class-sso.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/managers/class-domain-manager.php b/inc/managers/class-domain-manager.php index a8985e6d..9d6a3d8d 100644 --- a/inc/managers/class-domain-manager.php +++ b/inc/managers/class-domain-manager.php @@ -534,7 +534,7 @@ public function add_sso_settings(): void { 'title' => __('Enable SSO Loading Overlay', 'ultimate-multisite'), 'desc' => __('When active, a loading overlay will be added on-top of the site currently being viewed while the SSO auth loopback is performed on the background.', 'ultimate-multisite'), 'type' => 'toggle', - 'default' => 1, + 'default' => 0, 'require' => [ 'enable_sso' => true, ], diff --git a/inc/sso/class-sso.php b/inc/sso/class-sso.php index e1fa5f14..c0a4c9b3 100644 --- a/inc/sso/class-sso.php +++ b/inc/sso/class-sso.php @@ -764,7 +764,7 @@ public function enqueue_script(): void { 'expiration_in_days' => 5 / (24 * 60), // cookie expires in 5 mins. 'filtered_url' => remove_query_arg($removable_query_args, $this->get_current_url()), 'img_folder' => dirname((string) wu_get_asset('img', 'img')), - 'use_overlay' => $this->get_setting('enable_sso_loading_overlay', true), + 'use_overlay' => $this->get_setting('enable_sso_loading_overlay', false), ]; wp_localize_script('wu-sso', 'wu_sso_config', $options); From 30644c99254e1afc70e7420a3041165c3f0c283e Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 16 Feb 2026 16:38:41 -0700 Subject: [PATCH 07/10] Fix inline login prompt stability and add validation error for existing emails The inline login prompt on the register page had several issues: it would disappear when interacting with other form fields because check_user_exists shared global state across email/username fields, and the DOM-cloning pattern in setup_inline_login_handlers conflicted with browser autofill. Now only email changes can dismiss the email prompt, handlers attach once via a data attribute flag, the close button is removed, and a validation error is shown under the email field when the address already exists. Co-Authored-By: Claude Opus 4.6 --- assets/js/checkout.js | 138 +++++++++++------- assets/js/checkout.min.js | 2 +- inc/checkout/class-checkout.php | 1 + .../class-signup-field-email.php | 1 - .../checkout/partials/inline-login-prompt.php | 12 +- 5 files changed, 91 insertions(+), 63 deletions(-) diff --git a/assets/js/checkout.js b/assets/js/checkout.js index 6569ab73..7ecf40cf 100644 --- a/assets/js/checkout.js +++ b/assets/js/checkout.js @@ -812,10 +812,18 @@ }, 500), check_user_exists(field_type, value) { + // Don't let other field checks interfere with an active email prompt + if (this.show_login_prompt && this.login_prompt_field === 'email' && field_type !== 'email') { + return; + } + // Don't check if value is too short if (! value || value.length < 3) { - this.show_login_prompt = false; + if (this.login_prompt_field === field_type) { + this.show_login_prompt = false; + this.remove_field_error(field_type === 'email' ? 'email_address' : 'username'); + } return; @@ -839,16 +847,23 @@ that.show_login_prompt = true; that.login_prompt_field = field_type; - } else { + that.add_field_error(field_type === 'email' ? 'email_address' : 'username', wu_checkout.i18n.email_exists); + + } else if (that.login_prompt_field === field_type) { that.show_login_prompt = false; + that.remove_field_error(field_type === 'email' ? 'email_address' : 'username'); } }, function(error) { that.checking_user_exists = false; - that.show_login_prompt = false; + + if (that.login_prompt_field === field_type) { + that.show_login_prompt = false; + that.remove_field_error(field_type === 'email' ? 'email_address' : 'username'); + } }); @@ -913,6 +928,25 @@ return false; + }, + add_field_error(field_code, message) { + + this.remove_field_error(field_code); + + this.errors.push({ + code: field_code, + message, + }); + + }, + remove_field_error(field_code) { + + this.errors = this.errors.filter(function(e) { + + return e.code !== field_code; + + }); + }, dismiss_login_prompt() { @@ -928,6 +962,19 @@ // Setup handlers for both email and username field types [ 'email', 'username' ].forEach(function(fieldType) { + const loginPromptContainer = document.getElementById('wu-inline-login-prompt-' + fieldType); + + if (! loginPromptContainer) { + return; + } + + // Only attach handlers once per container + if (loginPromptContainer.dataset.wuHandlersAttached) { + return; + } + + loginPromptContainer.dataset.wuHandlersAttached = '1'; + const passwordField = document.getElementById('wu-inline-login-password-' + fieldType); const submitButton = document.getElementById('wu-inline-login-submit-' + fieldType); @@ -935,33 +982,36 @@ return; } - const dismissButton = document.getElementById('wu-dismiss-login-prompt-' + fieldType); const errorDiv = document.getElementById('wu-login-error-' + fieldType); - const loginPromptContainer = document.getElementById('wu-inline-login-prompt-' + fieldType); - // Remove any existing listeners to avoid duplicates - const newSubmitButton = submitButton.cloneNode(true); - submitButton.parentNode.replaceChild(newSubmitButton, submitButton); + function showError(message) { + + errorDiv.textContent = message; + errorDiv.classList.remove('wu-hidden'); + + } + + function hideError() { + + errorDiv.classList.add('wu-hidden'); + + } - const newPasswordField = passwordField.cloneNode(true); - passwordField.parentNode.replaceChild(newPasswordField, passwordField); function handleError(error) { - newSubmitButton.disabled = false; - newSubmitButton.textContent = wu_checkout.i18n.sign_in || 'Sign in'; + submitButton.disabled = false; + submitButton.textContent = wu_checkout.i18n.sign_in || 'Sign in'; if (error.data && error.data.message) { - errorDiv.textContent = error.data.message; + showError(error.data.message); } else { - errorDiv.textContent = wu_checkout.i18n.login_failed || 'Login failed. Please try again.'; + showError(wu_checkout.i18n.login_failed || 'Login failed. Please try again.'); } - errorDiv.style.display = 'block'; - } function handleLogin(e) { @@ -970,20 +1020,19 @@ e.stopPropagation(); e.stopImmediatePropagation(); - const password = newPasswordField.value; + const password = passwordField.value; if (! password) { - errorDiv.textContent = wu_checkout.i18n.password_required || 'Password is required'; - errorDiv.style.display = 'block'; + showError(wu_checkout.i18n.password_required || 'Password is required'); return false; } - newSubmitButton.disabled = true; - newSubmitButton.innerHTML = '' + (wu_checkout.i18n.logging_in || 'Logging in...'); - errorDiv.style.display = 'none'; + submitButton.disabled = true; + submitButton.innerHTML = '' + (wu_checkout.i18n.logging_in || 'Logging in...'); + hideError(); const username_or_email = fieldType === 'email' ? that.email_address : that.username; @@ -1014,31 +1063,27 @@ } // Stop all events from bubbling out of the login prompt - if (loginPromptContainer) { + loginPromptContainer.addEventListener('click', function(e) { - loginPromptContainer.addEventListener('click', function(e) { - - e.stopPropagation(); - - }); + e.stopPropagation(); - loginPromptContainer.addEventListener('keydown', function(e) { + }); - e.stopPropagation(); + loginPromptContainer.addEventListener('keydown', function(e) { - }); + e.stopPropagation(); - loginPromptContainer.addEventListener('keyup', function(e) { + }); - e.stopPropagation(); + loginPromptContainer.addEventListener('keyup', function(e) { - }); + e.stopPropagation(); - } + }); - newSubmitButton.addEventListener('click', handleLogin); + submitButton.addEventListener('click', handleLogin); - newPasswordField.addEventListener('keydown', function(e) { + passwordField.addEventListener('keydown', function(e) { if (e.key === 'Enter') { @@ -1048,20 +1093,6 @@ }); - if (dismissButton) { - - dismissButton.addEventListener('click', function(e) { - - e.preventDefault(); - e.stopPropagation(); - that.show_login_prompt = false; - that.inline_login_password = ''; - newPasswordField.value = ''; - - }); - - } - }); }, @@ -1163,6 +1194,11 @@ }, watch: { + email_address: _.debounce(function(new_value) { + + this.check_user_exists('email', new_value); + + }, 500), products(new_value, old_value) { this.on_change_product(new_value, old_value); diff --git a/assets/js/checkout.min.js b/assets/js/checkout.min.js index aab73c4e..9271dc49 100644 --- a/assets/js/checkout.min.js +++ b/assets/js/checkout.min.js @@ -1 +1 @@ -((r,n,s)=>{window.history.replaceState&&window.history.replaceState(null,null,wu_checkout.baseurl),n.addAction("wu_on_create_order","nextpress/wp-ultimo",function(e,t){void 0!==t.order.extra.template_id&&t.order.extra.template_id&&(e.template_id=t.order.extra.template_id)}),n.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(e){void 0!==window.wu_auto_submittable_field&&window.wu_auto_submittable_field&&e.$watch(window.wu_auto_submittable_field,function(){jQuery(this.$el).submit()},{deep:!0})}),n.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(t){wu_create_cookie("wu_template",""),wu_create_cookie("wu_selected_products",""),wu_listen_to_cookie_change("wu_template",function(e){e&&(t.template_id=e)})}),r(document).on("click",'[href|="#wu-checkout-add"]',function(e){e.preventDefault();var e=r(this),t=e.attr("href").split("#").pop().replace("wu-checkout-add-","");"undefined"!=typeof wu_checkout_form&&-1===wu_checkout_form.products.indexOf(t)&&(wu_checkout_form.add_product(t),e.html(wu_checkout.i18n.added_to_order))}),window.addEventListener("pageshow",function(e){e.persisted&&this.window.wu_checkout_form&&this.window.wu_checkout_form.unblock()}),r(document).ready(function(){var e;void 0!==window.Vue&&(Object.defineProperty(Vue.prototype,"$moment",{value:moment}),e={plan:(e=function(e){return isNaN(e)?e:parseInt(e,10)})(wu_checkout.plan),errors:[],order:wu_checkout.order,products:s.map(wu_checkout.products,e),template_id:wu_checkout.template_id,template_category:"",gateway:wu_checkout.gateway,request_billing_address:wu_checkout.request_billing_address,country:wu_checkout.country,state:"",city:"",site_title:wu_checkout.site_title||"",site_url:wu_checkout.site_url,site_domain:wu_checkout.site_domain,is_subdomain:wu_checkout.is_subdomain,discount_code:wu_checkout.discount_code,toggle_discount_code:0,payment_method:"",username:"",email_address:"",payment_id:wu_checkout.payment_id,membership_id:wu_checkout.membership_id,cart_type:"new",auto_renew:1,duration:wu_checkout.duration,duration_unit:wu_checkout.duration_unit,prevent_submission:!1,valid_password:!0,stored_templates:{},state_list:[],city_list:[],labels:{},show_login_prompt:!1,login_prompt_field:"",checking_user_exists:!1,logging_in:!1,login_error:"",inline_login_password:"",custom_amounts:wu_checkout.custom_amounts||{},pwyw_recurring:wu_checkout.pwyw_recurring||{}},n.applyFilters("wu_before_form_init",e),jQuery("#wu_form").length)&&(Vue.component("colorPicker",{props:["value"],template:'',mounted(){let o=this;r(this.$el).val(this.value).wpColorPicker({width:200,defaultColor:this.value,change(e,t){o.$emit("input",t.color.toString())}})},watch:{value(e){r(this.$el).wpColorPicker("color",e)}},destroyed(){r(this.$el).off().wpColorPicker("destroy")}}),window.wu_checkout_form=new Vue({el:"#wu_form",data:e,directives:{init:{bind(e,t,o){o.context[t.arg]=t.value}}},components:{dynamic:{functional:!0,template:"#dynamic",props:["template"],render(e,t){t=t.props.template;return e(t?{template:t}:"
nbsp;
")}}},computed:{hooks(){return wp.hooks},unique_products(){return s.uniq(this.products,!1,e=>parseInt(e,10))}},methods:{debounce(e){return s.debounce(e,200,!0)},open_url(e,t="_blank"){window.open(e,t)},get_template(e,t){void 0===t.id&&(t.id="default");var o=e+"/"+t.id;return void 0!==this.stored_templates[o]?this.stored_templates[o]:(o=this.hooks.applyFilters("wu_before_template_fetch",{duration:this.duration,duration_unit:this.duration_unit,products:this.products,...t},this),this.fetch_template(e,o),'
'+wu_checkout.i18n.loading+"
")},reset_templates(r){if(void 0===r)this.stored_templates={};else{let i={};s.forEach(this.stored_templates,function(e,t){var o=t.toString().substr(0,t.toString().indexOf("/"));!1===s.contains(r,o)&&(i[t]=e)}),this.stored_templates=i}},fetch_template(o,i){let r=this;void 0===i.id&&(i.id="default"),this.request("wu_render_field_template",{template:o,attributes:i},function(e){var t=o+"/"+i.id;e.success?Vue.set(r.stored_templates,t,e.data.html):Vue.set(r.stored_templates,t,"
"+e.data[0].message+"
")})},go_back(){this.block(),window.history.back()},set_prevent_submission(e){this.$nextTick(function(){this.prevent_submission=e})},remove_product(t,o){this.products=s.filter(this.products,function(e){return e!=t&&e!=o})},add_plan(e){this.plan&&this.remove_product(this.plan),this.plan=e,this.add_product(e)},add_product(e){this.products.push(e)},has_product(e){return-1',overlayCSS:{backgroundColor:e||"#ffffff",opacity:.6},css:{padding:0,margin:0,width:"50%",fontSize:"14px !important",top:"40%",left:"35%",textAlign:"center",color:"#000",border:"none",backgroundColor:"none",cursor:"wait"}})},unblock(){jQuery(this.$el).wu_unblock()},request(e,t,o,i){var r="wu_validate_form"===e||"wu_create_order"===e||"wu_render_field_template"===e||"wu_check_user_exists"===e||"wu_inline_login"===e?wu_checkout.late_ajaxurl:wu_checkout.ajaxurl;jQuery.ajax({method:"POST",url:r+"&action="+e,data:t,success:o,error:i})},init_password_strength(){let t=this;var e=jQuery("#field-password");e.length&&void 0!==window.WU_PasswordStrength&&(this.valid_password=!1,this.password_strength_checker=new window.WU_PasswordStrength({pass1:e,result:jQuery("#pass-strength-result"),onValidityChange(e){t.valid_password=e}}))},check_user_exists_debounced:s.debounce(function(e,t){this.check_user_exists(e,t)},500),check_user_exists(o,e){if(!e||e.length<3)this.show_login_prompt=!1;else{this.checking_user_exists=!0,this.login_error="";let t=this;this.request("wu_check_user_exists",{field_type:o,value:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.checking_user_exists=!1,e.success&&e.data.exists?(t.show_login_prompt=!0,t.login_prompt_field=o):t.show_login_prompt=!1},function(e){t.checking_user_exists=!1,t.show_login_prompt=!1})}},handle_inline_login(e){if(e&&(e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation()),this.inline_login_password){this.logging_in=!0,this.login_error="";let t=this;e="email"===this.login_prompt_field?this.email_address||"":this.username||"";this.request("wu_inline_login",{username_or_email:e,password:this.inline_login_password,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.logging_in=!1,e.success&&window.location.reload()},function(e){t.logging_in=!1,e.responseJSON&&e.responseJSON.data&&e.responseJSON.data.message?t.login_error=e.responseJSON.data.message:t.login_error=wu_checkout.i18n.login_failed||"Login failed. Please try again."})}else this.login_error=wu_checkout.i18n.password_required||"Password is required";return!1},dismiss_login_prompt(){this.show_login_prompt=!1,this.inline_login_password="",this.login_error=""},setup_inline_login_handlers(){let d=this;["email","username"].forEach(function(n){var e=document.getElementById("wu-inline-login-password-"+n),t=document.getElementById("wu-inline-login-submit-"+n);if(e&&t){var s=document.getElementById("wu-dismiss-login-prompt-"+n);let o=document.getElementById("wu-login-error-"+n);var a=document.getElementById("wu-inline-login-prompt-"+n);let i=t.cloneNode(!0),r=(t.parentNode.replaceChild(i,t),e.cloneNode(!0));function u(e){i.disabled=!1,i.textContent=wu_checkout.i18n.sign_in||"Sign in",e.data&&e.data.message?o.textContent=e.data.message:o.textContent=wu_checkout.i18n.login_failed||"Login failed. Please try again.",o.style.display="block"}function _(e){e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation();e=r.value;if(!e)return o.textContent=wu_checkout.i18n.password_required||"Password is required",!(o.style.display="block");i.disabled=!0,i.innerHTML=''+(wu_checkout.i18n.logging_in||"Logging in..."),o.style.display="none";var t="email"===n?d.email_address:d.username;return jQuery.ajax({method:"POST",url:wu_checkout.late_ajaxurl+"&action=wu_inline_login",data:{username_or_email:t,password:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},success(e){e.success?window.location.reload():u(e)},error:u}),!1}e.parentNode.replaceChild(r,e),a&&(a.addEventListener("click",function(e){e.stopPropagation()}),a.addEventListener("keydown",function(e){e.stopPropagation()}),a.addEventListener("keyup",function(e){e.stopPropagation()})),i.addEventListener("click",_),r.addEventListener("keydown",function(e){"Enter"===e.key&&_(e)}),s&&s.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),d.show_login_prompt=!1,d.inline_login_password="",r.value=""})}})}},updated(){this.$nextTick(function(){n.doAction("wu_on_form_updated",this),wu_initialize_tooltip(),this.setup_inline_login_handlers(),!this.password_strength_checker&&jQuery("#field-password").length&&this.init_password_strength()})},mounted(){let i=this;jQuery(this.$el).on("click",function(e){r(this).data("submited_via",r(e.target))}),jQuery(this.$el).on("submit",async function(e){e.preventDefault();var t,e=jQuery(this).data("submited_via");e&&((t=jQuery("")).attr("type","hidden"),t.attr("name",e.attr("name")),t.attr("value",e.val()),jQuery(this).append(t)),i.block();try{var o=[];await Promise.all(n.applyFilters("wu_before_form_submitted",o,i,i.gateway))}catch(e){return i.errors=[],i.errors.push({code:"before-submit-error",message:e.message}),i.unblock(),void i.handle_errors(e)}i.validate_form(),n.doAction("wu_on_form_submitted",i,i.gateway)}),this.create_order(),n.doAction("wu_checkout_loaded",this),n.doAction("wu_on_change_gateway",this.gateway,this.gateway),this.init_password_strength(),wu_initialize_tooltip()},watch:{products(e,t){this.on_change_product(e,t)},toggle_discount_code(e){e||(this.discount_code="")},discount_code(e,t){this.on_change_discount_code(e,t)},gateway(e,t){this.on_change_gateway(e,t)},country(e,t){this.state="",this.on_change_country(e,t)},state(e,t){this.city="",this.on_change_state(e,t)},city(e,t){this.on_change_city(e,t)},duration(e,t){this.on_change_duration(e,t)},duration_unit(e,t){this.on_change_duration_unit(e,t)}}}))})})(jQuery,wp.hooks,_); \ No newline at end of file +((r,n,s)=>{window.history.replaceState&&window.history.replaceState(null,null,wu_checkout.baseurl),n.addAction("wu_on_create_order","nextpress/wp-ultimo",function(e,t){void 0!==t.order.extra.template_id&&t.order.extra.template_id&&(e.template_id=t.order.extra.template_id)}),n.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(e){void 0!==window.wu_auto_submittable_field&&window.wu_auto_submittable_field&&e.$watch(window.wu_auto_submittable_field,function(){jQuery(this.$el).submit()},{deep:!0})}),n.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(t){wu_create_cookie("wu_template",""),wu_create_cookie("wu_selected_products",""),wu_listen_to_cookie_change("wu_template",function(e){e&&(t.template_id=e)})}),r(document).on("click",'[href|="#wu-checkout-add"]',function(e){e.preventDefault();var e=r(this),t=e.attr("href").split("#").pop().replace("wu-checkout-add-","");"undefined"!=typeof wu_checkout_form&&-1===wu_checkout_form.products.indexOf(t)&&(wu_checkout_form.add_product(t),e.html(wu_checkout.i18n.added_to_order))}),window.addEventListener("pageshow",function(e){e.persisted&&this.window.wu_checkout_form&&this.window.wu_checkout_form.unblock()}),r(document).ready(function(){var e;void 0!==window.Vue&&(Object.defineProperty(Vue.prototype,"$moment",{value:moment}),e={plan:(e=function(e){return isNaN(e)?e:parseInt(e,10)})(wu_checkout.plan),errors:[],order:wu_checkout.order,products:s.map(wu_checkout.products,e),template_id:wu_checkout.template_id,template_category:"",gateway:wu_checkout.gateway,request_billing_address:wu_checkout.request_billing_address,country:wu_checkout.country,state:"",city:"",site_title:wu_checkout.site_title||"",site_url:wu_checkout.site_url,site_domain:wu_checkout.site_domain,is_subdomain:wu_checkout.is_subdomain,discount_code:wu_checkout.discount_code,toggle_discount_code:0,payment_method:"",username:"",email_address:"",payment_id:wu_checkout.payment_id,membership_id:wu_checkout.membership_id,cart_type:"new",auto_renew:1,duration:wu_checkout.duration,duration_unit:wu_checkout.duration_unit,prevent_submission:!1,valid_password:!0,stored_templates:{},state_list:[],city_list:[],labels:{},show_login_prompt:!1,login_prompt_field:"",checking_user_exists:!1,logging_in:!1,login_error:"",inline_login_password:"",custom_amounts:wu_checkout.custom_amounts||{},pwyw_recurring:wu_checkout.pwyw_recurring||{}},n.applyFilters("wu_before_form_init",e),jQuery("#wu_form").length)&&(Vue.component("colorPicker",{props:["value"],template:'',mounted(){let o=this;r(this.$el).val(this.value).wpColorPicker({width:200,defaultColor:this.value,change(e,t){o.$emit("input",t.color.toString())}})},watch:{value(e){r(this.$el).wpColorPicker("color",e)}},destroyed(){r(this.$el).off().wpColorPicker("destroy")}}),window.wu_checkout_form=new Vue({el:"#wu_form",data:e,directives:{init:{bind(e,t,o){o.context[t.arg]=t.value}}},components:{dynamic:{functional:!0,template:"#dynamic",props:["template"],render(e,t){t=t.props.template;return e(t?{template:t}:"
nbsp;
")}}},computed:{hooks(){return wp.hooks},unique_products(){return s.uniq(this.products,!1,e=>parseInt(e,10))}},methods:{debounce(e){return s.debounce(e,200,!0)},open_url(e,t="_blank"){window.open(e,t)},get_template(e,t){void 0===t.id&&(t.id="default");var o=e+"/"+t.id;return void 0!==this.stored_templates[o]?this.stored_templates[o]:(o=this.hooks.applyFilters("wu_before_template_fetch",{duration:this.duration,duration_unit:this.duration_unit,products:this.products,...t},this),this.fetch_template(e,o),'
'+wu_checkout.i18n.loading+"
")},reset_templates(r){if(void 0===r)this.stored_templates={};else{let i={};s.forEach(this.stored_templates,function(e,t){var o=t.toString().substr(0,t.toString().indexOf("/"));!1===s.contains(r,o)&&(i[t]=e)}),this.stored_templates=i}},fetch_template(o,i){let r=this;void 0===i.id&&(i.id="default"),this.request("wu_render_field_template",{template:o,attributes:i},function(e){var t=o+"/"+i.id;e.success?Vue.set(r.stored_templates,t,e.data.html):Vue.set(r.stored_templates,t,"
"+e.data[0].message+"
")})},go_back(){this.block(),window.history.back()},set_prevent_submission(e){this.$nextTick(function(){this.prevent_submission=e})},remove_product(t,o){this.products=s.filter(this.products,function(e){return e!=t&&e!=o})},add_plan(e){this.plan&&this.remove_product(this.plan),this.plan=e,this.add_product(e)},add_product(e){this.products.push(e)},has_product(e){return-1',overlayCSS:{backgroundColor:e||"#ffffff",opacity:.6},css:{padding:0,margin:0,width:"50%",fontSize:"14px !important",top:"40%",left:"35%",textAlign:"center",color:"#000",border:"none",backgroundColor:"none",cursor:"wait"}})},unblock(){jQuery(this.$el).wu_unblock()},request(e,t,o,i){var r="wu_validate_form"===e||"wu_create_order"===e||"wu_render_field_template"===e||"wu_check_user_exists"===e||"wu_inline_login"===e?wu_checkout.late_ajaxurl:wu_checkout.ajaxurl;jQuery.ajax({method:"POST",url:r+"&action="+e,data:t,success:o,error:i})},init_password_strength(){let t=this;var e=jQuery("#field-password");e.length&&void 0!==window.WU_PasswordStrength&&(this.valid_password=!1,this.password_strength_checker=new window.WU_PasswordStrength({pass1:e,result:jQuery("#pass-strength-result"),onValidityChange(e){t.valid_password=e}}))},check_user_exists_debounced:s.debounce(function(e,t){this.check_user_exists(e,t)},500),check_user_exists(o,e){if(!e||e.length<3)this.login_prompt_field===o&&(this.show_login_prompt=!1);else{this.checking_user_exists=!0,this.login_error="";let t=this;this.request("wu_check_user_exists",{field_type:o,value:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.checking_user_exists=!1,e.success&&e.data.exists?(t.show_login_prompt=!0,t.login_prompt_field=o):t.login_prompt_field===o&&(t.show_login_prompt=!1)},function(e){t.checking_user_exists=!1,t.login_prompt_field===o&&(t.show_login_prompt=!1)})}},handle_inline_login(e){if(e&&(e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation()),this.inline_login_password){this.logging_in=!0,this.login_error="";let t=this;e="email"===this.login_prompt_field?this.email_address||"":this.username||"";this.request("wu_inline_login",{username_or_email:e,password:this.inline_login_password,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.logging_in=!1,e.success&&window.location.reload()},function(e){t.logging_in=!1,e.responseJSON&&e.responseJSON.data&&e.responseJSON.data.message?t.login_error=e.responseJSON.data.message:t.login_error=wu_checkout.i18n.login_failed||"Login failed. Please try again."})}else this.login_error=wu_checkout.i18n.password_required||"Password is required";return!1},dismiss_login_prompt(){this.show_login_prompt=!1,this.inline_login_password="",this.login_error=""},setup_inline_login_handlers(){let d=this;["email","username"].forEach(function(n){var e=document.getElementById("wu-inline-login-password-"+n),t=document.getElementById("wu-inline-login-submit-"+n);if(e&&t){var s=document.getElementById("wu-dismiss-login-prompt-"+n);let o=document.getElementById("wu-login-error-"+n);var a=document.getElementById("wu-inline-login-prompt-"+n);let i=t.cloneNode(!0),r=(t.parentNode.replaceChild(i,t),e.cloneNode(!0));function u(e){i.disabled=!1,i.textContent=wu_checkout.i18n.sign_in||"Sign in",e.data&&e.data.message?o.textContent=e.data.message:o.textContent=wu_checkout.i18n.login_failed||"Login failed. Please try again.",o.style.display="block"}function _(e){e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation();e=r.value;if(!e)return o.textContent=wu_checkout.i18n.password_required||"Password is required",!(o.style.display="block");i.disabled=!0,i.innerHTML=''+(wu_checkout.i18n.logging_in||"Logging in..."),o.style.display="none";var t="email"===n?d.email_address:d.username;return jQuery.ajax({method:"POST",url:wu_checkout.late_ajaxurl+"&action=wu_inline_login",data:{username_or_email:t,password:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},success(e){e.success?window.location.reload():u(e)},error:u}),!1}e.parentNode.replaceChild(r,e),a&&(a.addEventListener("click",function(e){e.stopPropagation()}),a.addEventListener("keydown",function(e){e.stopPropagation()}),a.addEventListener("keyup",function(e){e.stopPropagation()})),i.addEventListener("click",_),r.addEventListener("keydown",function(e){"Enter"===e.key&&_(e)}),s&&s.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),d.show_login_prompt=!1,d.inline_login_password="",r.value=""})}})}},updated(){this.$nextTick(function(){n.doAction("wu_on_form_updated",this),wu_initialize_tooltip(),this.setup_inline_login_handlers(),!this.password_strength_checker&&jQuery("#field-password").length&&this.init_password_strength()})},mounted(){let i=this;jQuery(this.$el).on("click",function(e){r(this).data("submited_via",r(e.target))}),jQuery(this.$el).on("submit",async function(e){e.preventDefault();var t,e=jQuery(this).data("submited_via");e&&((t=jQuery("")).attr("type","hidden"),t.attr("name",e.attr("name")),t.attr("value",e.val()),jQuery(this).append(t)),i.block();try{var o=[];await Promise.all(n.applyFilters("wu_before_form_submitted",o,i,i.gateway))}catch(e){return i.errors=[],i.errors.push({code:"before-submit-error",message:e.message}),i.unblock(),void i.handle_errors(e)}i.validate_form(),n.doAction("wu_on_form_submitted",i,i.gateway)}),this.create_order(),n.doAction("wu_checkout_loaded",this),n.doAction("wu_on_change_gateway",this.gateway,this.gateway),this.init_password_strength(),wu_initialize_tooltip()},watch:{products(e,t){this.on_change_product(e,t)},toggle_discount_code(e){e||(this.discount_code="")},discount_code(e,t){this.on_change_discount_code(e,t)},gateway(e,t){this.on_change_gateway(e,t)},country(e,t){this.state="",this.on_change_country(e,t)},state(e,t){this.city="",this.on_change_state(e,t)},city(e,t){this.on_change_city(e,t)},duration(e,t){this.on_change_duration(e,t)},duration_unit(e,t){this.on_change_duration_unit(e,t)}}}))})})(jQuery,wp.hooks,_); \ No newline at end of file diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index f261d375..adbede11 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -1834,6 +1834,7 @@ public function get_checkout_variables() { 'sign_in' => __('Sign in', 'ultimate-multisite'), 'forgot_password' => __('Forgot password?', 'ultimate-multisite'), 'cancel' => __('Cancel', 'ultimate-multisite'), + 'email_exists' => __('A customer with the same email address or username already exists.', 'ultimate-multisite'), ]; /* diff --git a/inc/checkout/signup-fields/class-signup-field-email.php b/inc/checkout/signup-fields/class-signup-field-email.php index 6f175abf..f0b61674 100644 --- a/inc/checkout/signup-fields/class-signup-field-email.php +++ b/inc/checkout/signup-fields/class-signup-field-email.php @@ -234,7 +234,6 @@ public function to_fields_array($attributes) { 'wrapper_classes' => wu_get_isset($attributes, 'wrapper_element_classes', ''), 'classes' => wu_get_isset($attributes, 'element_classes', ''), 'html_attr' => [ - '@blur' => "check_user_exists_debounced('email', email_address)", 'v-model' => 'email_address', ], 'wrapper_html_attr' => [ diff --git a/views/checkout/partials/inline-login-prompt.php b/views/checkout/partials/inline-login-prompt.php index fcaf96f3..f1305408 100644 --- a/views/checkout/partials/inline-login-prompt.php +++ b/views/checkout/partials/inline-login-prompt.php @@ -12,18 +12,10 @@ ?>
-
+

-
@@ -39,7 +31,7 @@ class="form-control wu-w-full" />
- -
    +
    • @@ -182,7 +182,7 @@ - + get_email_address()); ?> @@ -250,11 +250,11 @@
      -
      +
      Thumbnail of Site
      @@ -368,9 +368,9 @@ class="sm:wu-w-12 sm:wu-h-12 wu-mb-4 sm:wu-mb-0 wu-rounded" -
      +
      - +
      From 134441278cbfc51d0886be550fc5e0206fcec260 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 16 Feb 2026 19:02:08 -0700 Subject: [PATCH 09/10] Fix i18n warnings and berlindb hooks generator undefined variable - Fix undefined $hook_lines in generate-berlindb-hooks.php causing PHP warnings - Replace misused _n() calls with conditionals to resolve makepot warnings about missing singular placeholders and strings without translatable content - Add missing translators comment for Stripe webhook error string - Standardize translator comments across hosting provider integrations - Update POT file and readme.txt Co-Authored-By: Claude Opus 4.6 --- assets/js/checkout.min.js | 2 +- inc/checkout/class-line-item.php | 19 +- inc/documentation/berlindb-dynamic-hooks.php | 268 +++++------ inc/documentation/generate-berlindb-hooks.php | 12 + inc/gateways/class-base-stripe-gateway.php | 1 + .../closte/class-closte-domain-mapping.php | 4 +- .../class-serverpilot-domain-mapping.php | 4 +- .../class-customers-membership-list-table.php | 13 +- .../class-membership-list-table-widget.php | 16 +- .../class-membership-list-table.php | 16 +- inc/list-tables/class-product-list-table.php | 16 +- inc/models/class-checkout-form.php | 24 +- inc/models/class-membership.php | 45 +- inc/models/class-product.php | 45 +- lang/ultimate-multisite.pot | 415 ++++++++++-------- readme.txt | 15 +- 16 files changed, 519 insertions(+), 396 deletions(-) diff --git a/assets/js/checkout.min.js b/assets/js/checkout.min.js index 9271dc49..e5600b6b 100644 --- a/assets/js/checkout.min.js +++ b/assets/js/checkout.min.js @@ -1 +1 @@ -((r,n,s)=>{window.history.replaceState&&window.history.replaceState(null,null,wu_checkout.baseurl),n.addAction("wu_on_create_order","nextpress/wp-ultimo",function(e,t){void 0!==t.order.extra.template_id&&t.order.extra.template_id&&(e.template_id=t.order.extra.template_id)}),n.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(e){void 0!==window.wu_auto_submittable_field&&window.wu_auto_submittable_field&&e.$watch(window.wu_auto_submittable_field,function(){jQuery(this.$el).submit()},{deep:!0})}),n.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(t){wu_create_cookie("wu_template",""),wu_create_cookie("wu_selected_products",""),wu_listen_to_cookie_change("wu_template",function(e){e&&(t.template_id=e)})}),r(document).on("click",'[href|="#wu-checkout-add"]',function(e){e.preventDefault();var e=r(this),t=e.attr("href").split("#").pop().replace("wu-checkout-add-","");"undefined"!=typeof wu_checkout_form&&-1===wu_checkout_form.products.indexOf(t)&&(wu_checkout_form.add_product(t),e.html(wu_checkout.i18n.added_to_order))}),window.addEventListener("pageshow",function(e){e.persisted&&this.window.wu_checkout_form&&this.window.wu_checkout_form.unblock()}),r(document).ready(function(){var e;void 0!==window.Vue&&(Object.defineProperty(Vue.prototype,"$moment",{value:moment}),e={plan:(e=function(e){return isNaN(e)?e:parseInt(e,10)})(wu_checkout.plan),errors:[],order:wu_checkout.order,products:s.map(wu_checkout.products,e),template_id:wu_checkout.template_id,template_category:"",gateway:wu_checkout.gateway,request_billing_address:wu_checkout.request_billing_address,country:wu_checkout.country,state:"",city:"",site_title:wu_checkout.site_title||"",site_url:wu_checkout.site_url,site_domain:wu_checkout.site_domain,is_subdomain:wu_checkout.is_subdomain,discount_code:wu_checkout.discount_code,toggle_discount_code:0,payment_method:"",username:"",email_address:"",payment_id:wu_checkout.payment_id,membership_id:wu_checkout.membership_id,cart_type:"new",auto_renew:1,duration:wu_checkout.duration,duration_unit:wu_checkout.duration_unit,prevent_submission:!1,valid_password:!0,stored_templates:{},state_list:[],city_list:[],labels:{},show_login_prompt:!1,login_prompt_field:"",checking_user_exists:!1,logging_in:!1,login_error:"",inline_login_password:"",custom_amounts:wu_checkout.custom_amounts||{},pwyw_recurring:wu_checkout.pwyw_recurring||{}},n.applyFilters("wu_before_form_init",e),jQuery("#wu_form").length)&&(Vue.component("colorPicker",{props:["value"],template:'',mounted(){let o=this;r(this.$el).val(this.value).wpColorPicker({width:200,defaultColor:this.value,change(e,t){o.$emit("input",t.color.toString())}})},watch:{value(e){r(this.$el).wpColorPicker("color",e)}},destroyed(){r(this.$el).off().wpColorPicker("destroy")}}),window.wu_checkout_form=new Vue({el:"#wu_form",data:e,directives:{init:{bind(e,t,o){o.context[t.arg]=t.value}}},components:{dynamic:{functional:!0,template:"#dynamic",props:["template"],render(e,t){t=t.props.template;return e(t?{template:t}:"
      nbsp;
      ")}}},computed:{hooks(){return wp.hooks},unique_products(){return s.uniq(this.products,!1,e=>parseInt(e,10))}},methods:{debounce(e){return s.debounce(e,200,!0)},open_url(e,t="_blank"){window.open(e,t)},get_template(e,t){void 0===t.id&&(t.id="default");var o=e+"/"+t.id;return void 0!==this.stored_templates[o]?this.stored_templates[o]:(o=this.hooks.applyFilters("wu_before_template_fetch",{duration:this.duration,duration_unit:this.duration_unit,products:this.products,...t},this),this.fetch_template(e,o),'
      '+wu_checkout.i18n.loading+"
      ")},reset_templates(r){if(void 0===r)this.stored_templates={};else{let i={};s.forEach(this.stored_templates,function(e,t){var o=t.toString().substr(0,t.toString().indexOf("/"));!1===s.contains(r,o)&&(i[t]=e)}),this.stored_templates=i}},fetch_template(o,i){let r=this;void 0===i.id&&(i.id="default"),this.request("wu_render_field_template",{template:o,attributes:i},function(e){var t=o+"/"+i.id;e.success?Vue.set(r.stored_templates,t,e.data.html):Vue.set(r.stored_templates,t,"
      "+e.data[0].message+"
      ")})},go_back(){this.block(),window.history.back()},set_prevent_submission(e){this.$nextTick(function(){this.prevent_submission=e})},remove_product(t,o){this.products=s.filter(this.products,function(e){return e!=t&&e!=o})},add_plan(e){this.plan&&this.remove_product(this.plan),this.plan=e,this.add_product(e)},add_product(e){this.products.push(e)},has_product(e){return-1',overlayCSS:{backgroundColor:e||"#ffffff",opacity:.6},css:{padding:0,margin:0,width:"50%",fontSize:"14px !important",top:"40%",left:"35%",textAlign:"center",color:"#000",border:"none",backgroundColor:"none",cursor:"wait"}})},unblock(){jQuery(this.$el).wu_unblock()},request(e,t,o,i){var r="wu_validate_form"===e||"wu_create_order"===e||"wu_render_field_template"===e||"wu_check_user_exists"===e||"wu_inline_login"===e?wu_checkout.late_ajaxurl:wu_checkout.ajaxurl;jQuery.ajax({method:"POST",url:r+"&action="+e,data:t,success:o,error:i})},init_password_strength(){let t=this;var e=jQuery("#field-password");e.length&&void 0!==window.WU_PasswordStrength&&(this.valid_password=!1,this.password_strength_checker=new window.WU_PasswordStrength({pass1:e,result:jQuery("#pass-strength-result"),onValidityChange(e){t.valid_password=e}}))},check_user_exists_debounced:s.debounce(function(e,t){this.check_user_exists(e,t)},500),check_user_exists(o,e){if(!e||e.length<3)this.login_prompt_field===o&&(this.show_login_prompt=!1);else{this.checking_user_exists=!0,this.login_error="";let t=this;this.request("wu_check_user_exists",{field_type:o,value:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.checking_user_exists=!1,e.success&&e.data.exists?(t.show_login_prompt=!0,t.login_prompt_field=o):t.login_prompt_field===o&&(t.show_login_prompt=!1)},function(e){t.checking_user_exists=!1,t.login_prompt_field===o&&(t.show_login_prompt=!1)})}},handle_inline_login(e){if(e&&(e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation()),this.inline_login_password){this.logging_in=!0,this.login_error="";let t=this;e="email"===this.login_prompt_field?this.email_address||"":this.username||"";this.request("wu_inline_login",{username_or_email:e,password:this.inline_login_password,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.logging_in=!1,e.success&&window.location.reload()},function(e){t.logging_in=!1,e.responseJSON&&e.responseJSON.data&&e.responseJSON.data.message?t.login_error=e.responseJSON.data.message:t.login_error=wu_checkout.i18n.login_failed||"Login failed. Please try again."})}else this.login_error=wu_checkout.i18n.password_required||"Password is required";return!1},dismiss_login_prompt(){this.show_login_prompt=!1,this.inline_login_password="",this.login_error=""},setup_inline_login_handlers(){let d=this;["email","username"].forEach(function(n){var e=document.getElementById("wu-inline-login-password-"+n),t=document.getElementById("wu-inline-login-submit-"+n);if(e&&t){var s=document.getElementById("wu-dismiss-login-prompt-"+n);let o=document.getElementById("wu-login-error-"+n);var a=document.getElementById("wu-inline-login-prompt-"+n);let i=t.cloneNode(!0),r=(t.parentNode.replaceChild(i,t),e.cloneNode(!0));function u(e){i.disabled=!1,i.textContent=wu_checkout.i18n.sign_in||"Sign in",e.data&&e.data.message?o.textContent=e.data.message:o.textContent=wu_checkout.i18n.login_failed||"Login failed. Please try again.",o.style.display="block"}function _(e){e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation();e=r.value;if(!e)return o.textContent=wu_checkout.i18n.password_required||"Password is required",!(o.style.display="block");i.disabled=!0,i.innerHTML=''+(wu_checkout.i18n.logging_in||"Logging in..."),o.style.display="none";var t="email"===n?d.email_address:d.username;return jQuery.ajax({method:"POST",url:wu_checkout.late_ajaxurl+"&action=wu_inline_login",data:{username_or_email:t,password:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},success(e){e.success?window.location.reload():u(e)},error:u}),!1}e.parentNode.replaceChild(r,e),a&&(a.addEventListener("click",function(e){e.stopPropagation()}),a.addEventListener("keydown",function(e){e.stopPropagation()}),a.addEventListener("keyup",function(e){e.stopPropagation()})),i.addEventListener("click",_),r.addEventListener("keydown",function(e){"Enter"===e.key&&_(e)}),s&&s.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),d.show_login_prompt=!1,d.inline_login_password="",r.value=""})}})}},updated(){this.$nextTick(function(){n.doAction("wu_on_form_updated",this),wu_initialize_tooltip(),this.setup_inline_login_handlers(),!this.password_strength_checker&&jQuery("#field-password").length&&this.init_password_strength()})},mounted(){let i=this;jQuery(this.$el).on("click",function(e){r(this).data("submited_via",r(e.target))}),jQuery(this.$el).on("submit",async function(e){e.preventDefault();var t,e=jQuery(this).data("submited_via");e&&((t=jQuery("")).attr("type","hidden"),t.attr("name",e.attr("name")),t.attr("value",e.val()),jQuery(this).append(t)),i.block();try{var o=[];await Promise.all(n.applyFilters("wu_before_form_submitted",o,i,i.gateway))}catch(e){return i.errors=[],i.errors.push({code:"before-submit-error",message:e.message}),i.unblock(),void i.handle_errors(e)}i.validate_form(),n.doAction("wu_on_form_submitted",i,i.gateway)}),this.create_order(),n.doAction("wu_checkout_loaded",this),n.doAction("wu_on_change_gateway",this.gateway,this.gateway),this.init_password_strength(),wu_initialize_tooltip()},watch:{products(e,t){this.on_change_product(e,t)},toggle_discount_code(e){e||(this.discount_code="")},discount_code(e,t){this.on_change_discount_code(e,t)},gateway(e,t){this.on_change_gateway(e,t)},country(e,t){this.state="",this.on_change_country(e,t)},state(e,t){this.city="",this.on_change_state(e,t)},city(e,t){this.on_change_city(e,t)},duration(e,t){this.on_change_duration(e,t)},duration_unit(e,t){this.on_change_duration_unit(e,t)}}}))})})(jQuery,wp.hooks,_); \ No newline at end of file +((r,s,n)=>{window.history.replaceState&&window.history.replaceState(null,null,wu_checkout.baseurl),s.addAction("wu_on_create_order","nextpress/wp-ultimo",function(e,t){void 0!==t.order.extra.template_id&&t.order.extra.template_id&&(e.template_id=t.order.extra.template_id)}),s.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(e){void 0!==window.wu_auto_submittable_field&&window.wu_auto_submittable_field&&e.$watch(window.wu_auto_submittable_field,function(){jQuery(this.$el).submit()},{deep:!0})}),s.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(t){wu_create_cookie("wu_template",""),wu_create_cookie("wu_selected_products",""),wu_listen_to_cookie_change("wu_template",function(e){e&&(t.template_id=e)})}),r(document).on("click",'[href|="#wu-checkout-add"]',function(e){e.preventDefault();var e=r(this),t=e.attr("href").split("#").pop().replace("wu-checkout-add-","");"undefined"!=typeof wu_checkout_form&&-1===wu_checkout_form.products.indexOf(t)&&(wu_checkout_form.add_product(t),e.html(wu_checkout.i18n.added_to_order))}),window.addEventListener("pageshow",function(e){e.persisted&&this.window.wu_checkout_form&&this.window.wu_checkout_form.unblock()}),r(document).ready(function(){var e;void 0!==window.Vue&&(Object.defineProperty(Vue.prototype,"$moment",{value:moment}),e={plan:(e=function(e){return isNaN(e)?e:parseInt(e,10)})(wu_checkout.plan),errors:[],order:wu_checkout.order,products:n.map(wu_checkout.products,e),template_id:wu_checkout.template_id,template_category:"",gateway:wu_checkout.gateway,request_billing_address:wu_checkout.request_billing_address,country:wu_checkout.country,state:"",city:"",site_title:wu_checkout.site_title||"",site_url:wu_checkout.site_url,site_domain:wu_checkout.site_domain,is_subdomain:wu_checkout.is_subdomain,discount_code:wu_checkout.discount_code,toggle_discount_code:0,payment_method:"",username:"",email_address:"",payment_id:wu_checkout.payment_id,membership_id:wu_checkout.membership_id,cart_type:"new",auto_renew:1,duration:wu_checkout.duration,duration_unit:wu_checkout.duration_unit,prevent_submission:!1,valid_password:!0,stored_templates:{},state_list:[],city_list:[],labels:{},show_login_prompt:!1,login_prompt_field:"",checking_user_exists:!1,logging_in:!1,login_error:"",inline_login_password:"",custom_amounts:wu_checkout.custom_amounts||{},pwyw_recurring:wu_checkout.pwyw_recurring||{}},s.applyFilters("wu_before_form_init",e),jQuery("#wu_form").length)&&(Vue.component("colorPicker",{props:["value"],template:'',mounted(){let o=this;r(this.$el).val(this.value).wpColorPicker({width:200,defaultColor:this.value,change(e,t){o.$emit("input",t.color.toString())}})},watch:{value(e){r(this.$el).wpColorPicker("color",e)}},destroyed(){r(this.$el).off().wpColorPicker("destroy")}}),window.wu_checkout_form=new Vue({el:"#wu_form",data:e,directives:{init:{bind(e,t,o){o.context[t.arg]=t.value}}},components:{dynamic:{functional:!0,template:"#dynamic",props:["template"],render(e,t){t=t.props.template;return e(t?{template:t}:"
      nbsp;
      ")}}},computed:{hooks(){return wp.hooks},unique_products(){return n.uniq(this.products,!1,e=>parseInt(e,10))}},methods:{debounce(e){return n.debounce(e,200,!0)},open_url(e,t="_blank"){window.open(e,t)},get_template(e,t){void 0===t.id&&(t.id="default");var o=e+"/"+t.id;return void 0!==this.stored_templates[o]?this.stored_templates[o]:(o=this.hooks.applyFilters("wu_before_template_fetch",{duration:this.duration,duration_unit:this.duration_unit,products:this.products,...t},this),this.fetch_template(e,o),'
      '+wu_checkout.i18n.loading+"
      ")},reset_templates(r){if(void 0===r)this.stored_templates={};else{let i={};n.forEach(this.stored_templates,function(e,t){var o=t.toString().substr(0,t.toString().indexOf("/"));!1===n.contains(r,o)&&(i[t]=e)}),this.stored_templates=i}},fetch_template(o,i){let r=this;void 0===i.id&&(i.id="default"),this.request("wu_render_field_template",{template:o,attributes:i},function(e){var t=o+"/"+i.id;e.success?Vue.set(r.stored_templates,t,e.data.html):Vue.set(r.stored_templates,t,"
      "+e.data[0].message+"
      ")})},go_back(){this.block(),window.history.back()},set_prevent_submission(e){this.$nextTick(function(){this.prevent_submission=e})},remove_product(t,o){this.products=n.filter(this.products,function(e){return e!=t&&e!=o})},add_plan(e){this.plan&&this.remove_product(this.plan),this.plan=e,this.add_product(e)},add_product(e){this.products.push(e)},has_product(e){return-1',overlayCSS:{backgroundColor:e||"#ffffff",opacity:.6},css:{padding:0,margin:0,width:"50%",fontSize:"14px !important",top:"40%",left:"35%",textAlign:"center",color:"#000",border:"none",backgroundColor:"none",cursor:"wait"}})},unblock(){jQuery(this.$el).wu_unblock()},request(e,t,o,i){var r="wu_validate_form"===e||"wu_create_order"===e||"wu_render_field_template"===e||"wu_check_user_exists"===e||"wu_inline_login"===e?wu_checkout.late_ajaxurl:wu_checkout.ajaxurl;jQuery.ajax({method:"POST",url:r+"&action="+e,data:t,success:o,error:i})},init_password_strength(){let t=this;var e=jQuery("#field-password");e.length&&void 0!==window.WU_PasswordStrength&&(this.valid_password=!1,this.password_strength_checker=new window.WU_PasswordStrength({pass1:e,result:jQuery("#pass-strength-result"),onValidityChange(e){t.valid_password=e}}))},check_user_exists_debounced:n.debounce(function(e,t){this.check_user_exists(e,t)},500),check_user_exists(o,e){if(!this.show_login_prompt||"email"!==this.login_prompt_field||"email"===o)if(!e||e.length<3)this.login_prompt_field===o&&(this.show_login_prompt=!1,this.remove_field_error("email"===o?"email_address":"username"));else{this.checking_user_exists=!0,this.login_error="";let t=this;this.request("wu_check_user_exists",{field_type:o,value:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.checking_user_exists=!1,e.success&&e.data.exists?(t.show_login_prompt=!0,t.login_prompt_field=o,t.add_field_error("email"===o?"email_address":"username",wu_checkout.i18n.email_exists)):t.login_prompt_field===o&&(t.show_login_prompt=!1,t.remove_field_error("email"===o?"email_address":"username"))},function(e){t.checking_user_exists=!1,t.login_prompt_field===o&&(t.show_login_prompt=!1,t.remove_field_error("email"===o?"email_address":"username"))})}},handle_inline_login(e){if(e&&(e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation()),this.inline_login_password){this.logging_in=!0,this.login_error="";let t=this;e="email"===this.login_prompt_field?this.email_address||"":this.username||"";this.request("wu_inline_login",{username_or_email:e,password:this.inline_login_password,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.logging_in=!1,e.success&&window.location.reload()},function(e){t.logging_in=!1,e.responseJSON&&e.responseJSON.data&&e.responseJSON.data.message?t.login_error=e.responseJSON.data.message:t.login_error=wu_checkout.i18n.login_failed||"Login failed. Please try again."})}else this.login_error=wu_checkout.i18n.password_required||"Password is required";return!1},add_field_error(e,t){this.remove_field_error(e),this.errors.push({code:e,message:t})},remove_field_error(t){this.errors=this.errors.filter(function(e){return e.code!==t})},dismiss_login_prompt(){this.show_login_prompt=!1,this.inline_login_password="",this.login_error=""},setup_inline_login_handlers(){let u=this;["email","username"].forEach(function(s){var e=document.getElementById("wu-inline-login-prompt-"+s);if(e&&!e.dataset.wuHandlersAttached){e.dataset.wuHandlersAttached="1";let i=document.getElementById("wu-inline-login-password-"+s),r=document.getElementById("wu-inline-login-submit-"+s);if(i&&r){let o=document.getElementById("wu-login-error-"+s);function n(e){o.textContent=e,o.classList.remove("wu-hidden")}function a(e){r.disabled=!1,r.textContent=wu_checkout.i18n.sign_in||"Sign in",e.data&&e.data.message?n(e.data.message):n(wu_checkout.i18n.login_failed||"Login failed. Please try again.")}function t(e){e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation();var t,e=i.value;return e?(r.disabled=!0,r.innerHTML=''+(wu_checkout.i18n.logging_in||"Logging in..."),o.classList.add("wu-hidden"),t="email"===s?u.email_address:u.username,jQuery.ajax({method:"POST",url:wu_checkout.late_ajaxurl+"&action=wu_inline_login",data:{username_or_email:t,password:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},success(e){e.success?window.location.reload():a(e)},error:a})):n(wu_checkout.i18n.password_required||"Password is required"),!1}e.addEventListener("click",function(e){e.stopPropagation()}),e.addEventListener("keydown",function(e){e.stopPropagation()}),e.addEventListener("keyup",function(e){e.stopPropagation()}),r.addEventListener("click",t),i.addEventListener("keydown",function(e){"Enter"===e.key&&t(e)})}}})}},updated(){this.$nextTick(function(){s.doAction("wu_on_form_updated",this),wu_initialize_tooltip(),this.setup_inline_login_handlers(),!this.password_strength_checker&&jQuery("#field-password").length&&this.init_password_strength()})},mounted(){let i=this;jQuery(this.$el).on("click",function(e){r(this).data("submited_via",r(e.target))}),jQuery(this.$el).on("submit",async function(e){e.preventDefault();var t,e=jQuery(this).data("submited_via");e&&((t=jQuery("")).attr("type","hidden"),t.attr("name",e.attr("name")),t.attr("value",e.val()),jQuery(this).append(t)),i.block();try{var o=[];await Promise.all(s.applyFilters("wu_before_form_submitted",o,i,i.gateway))}catch(e){return i.errors=[],i.errors.push({code:"before-submit-error",message:e.message}),i.unblock(),void i.handle_errors(e)}i.validate_form(),s.doAction("wu_on_form_submitted",i,i.gateway)}),this.create_order(),s.doAction("wu_checkout_loaded",this),s.doAction("wu_on_change_gateway",this.gateway,this.gateway),this.init_password_strength(),wu_initialize_tooltip()},watch:{email_address:n.debounce(function(e){this.check_user_exists("email",e)},500),products(e,t){this.on_change_product(e,t)},toggle_discount_code(e){e||(this.discount_code="")},discount_code(e,t){this.on_change_discount_code(e,t)},gateway(e,t){this.on_change_gateway(e,t)},country(e,t){this.state="",this.on_change_country(e,t)},state(e,t){this.city="",this.on_change_state(e,t)},city(e,t){this.on_change_city(e,t)},duration(e,t){this.on_change_duration(e,t)},duration_unit(e,t){this.on_change_duration_unit(e,t)}}}))})})(jQuery,wp.hooks,_); \ No newline at end of file diff --git a/inc/checkout/class-line-item.php b/inc/checkout/class-line-item.php index 6e5d56e7..5bbf27ae 100644 --- a/inc/checkout/class-line-item.php +++ b/inc/checkout/class-line-item.php @@ -1098,12 +1098,19 @@ public function get_recurring_description() { return ''; } - $description = sprintf( - // translators: %1$s the duration, and %2$s the duration unit (day, week, month, etc) - _n('%2$s', 'every %1$s %2$s', $this->get_duration(), 'ultimate-multisite'), // phpcs:ignore - $this->get_duration(), - wu_get_translatable_string(($this->get_duration() <= 1 ? $this->get_duration_unit() : $this->get_duration_unit() . 's')) - ); + $duration = $this->get_duration(); + $duration_unit = wu_get_translatable_string($duration <= 1 ? $this->get_duration_unit() : $this->get_duration_unit() . 's'); + + if ($duration <= 1) { + $description = $duration_unit; + } else { + $description = sprintf( + // translators: %1$s: the duration number, %2$s: the duration unit (days, weeks, months, etc). + __('every %1$s %2$s', 'ultimate-multisite'), + $duration, + $duration_unit + ); + } return $description; } diff --git a/inc/documentation/berlindb-dynamic-hooks.php b/inc/documentation/berlindb-dynamic-hooks.php index 024ecb83..94fca5da 100644 --- a/inc/documentation/berlindb-dynamic-hooks.php +++ b/inc/documentation/berlindb-dynamic-hooks.php @@ -37,7 +37,7 @@ * Fires before blogs are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:883 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -47,7 +47,7 @@ * Fires after blogs query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1159 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -57,7 +57,7 @@ * Filters the SQL clauses for a blogs query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:994 * * @param array $clauses { * Associative array of SQL clause strings. @@ -78,7 +78,7 @@ * Filters the columns to search when performing a blogs search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1306 * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -91,7 +91,7 @@ * Filters the found blogs after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1595 * * @param object[] $items The array of found blog objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -103,7 +103,7 @@ * Filters a single blog item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2068 * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -115,7 +115,7 @@ * Filters the FOUND_ROWS() query for blogs. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:640 * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -132,7 +132,7 @@ * column for a customer row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous email verification value. * @param string $new_value The new email verification value. @@ -147,7 +147,7 @@ * column for a customer row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous has trialed value. * @param int $new_value The new has trialed value. @@ -162,7 +162,7 @@ * column for a customer row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous vip value. * @param int $new_value The new vip value. @@ -174,7 +174,7 @@ * Fires before customers are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:883 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -184,7 +184,7 @@ * Fires after customers query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1159 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -194,7 +194,7 @@ * Filters the SQL clauses for a customers query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:994 * * @param array $clauses { * Associative array of SQL clause strings. @@ -215,7 +215,7 @@ * Filters the columns to search when performing a customers search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1306 * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -228,7 +228,7 @@ * Filters the found customers after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1595 * * @param object[] $items The array of found customer objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -240,7 +240,7 @@ * Filters a single customer item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2068 * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -252,7 +252,7 @@ * Filters the FOUND_ROWS() query for customers. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:640 * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -269,7 +269,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous code value. * @param string $new_value The new code value. @@ -284,7 +284,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous uses value. * @param int $new_value The new uses value. @@ -299,7 +299,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous max uses value. * @param int $new_value The new max uses value. @@ -314,7 +314,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous apply to renewals value. * @param int $new_value The new apply to renewals value. @@ -329,7 +329,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous type value. * @param string $new_value The new type value. @@ -344,7 +344,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous value value. * @param string $new_value The new value value. @@ -359,7 +359,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous setup fee type value. * @param string $new_value The new setup fee type value. @@ -374,7 +374,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous setup fee value value. * @param string $new_value The new setup fee value value. @@ -389,7 +389,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous active value. * @param int $new_value The new active value. @@ -404,7 +404,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous date start value. * @param string $new_value The new date start value. @@ -419,7 +419,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous date expiration value. * @param string $new_value The new date expiration value. @@ -431,7 +431,7 @@ * Fires before discount codes are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:883 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -441,7 +441,7 @@ * Fires after discount codes query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1159 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -451,7 +451,7 @@ * Filters the SQL clauses for a discount codes query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:994 * * @param array $clauses { * Associative array of SQL clause strings. @@ -472,7 +472,7 @@ * Filters the columns to search when performing a discount codes search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1306 * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -485,7 +485,7 @@ * Filters the found discount codes after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1595 * * @param object[] $items The array of found discount code objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -497,7 +497,7 @@ * Filters a single discount code item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2068 * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -509,7 +509,7 @@ * Filters the FOUND_ROWS() query for discount codes. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:640 * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -526,7 +526,7 @@ * column for a domain row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous domain value. * @param string $new_value The new domain value. @@ -541,7 +541,7 @@ * column for a domain row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous active value. * @param int $new_value The new active value. @@ -556,7 +556,7 @@ * column for a domain row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous primary domain value. * @param int $new_value The new primary domain value. @@ -571,7 +571,7 @@ * column for a domain row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous secure value. * @param int $new_value The new secure value. @@ -586,7 +586,7 @@ * column for a domain row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous stage value. * @param string $new_value The new stage value. @@ -598,7 +598,7 @@ * Fires before domains are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:883 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -608,7 +608,7 @@ * Fires after domains query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1159 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -618,7 +618,7 @@ * Filters the SQL clauses for a domains query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:994 * * @param array $clauses { * Associative array of SQL clause strings. @@ -639,7 +639,7 @@ * Filters the columns to search when performing a domains search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1306 * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -652,7 +652,7 @@ * Filters the found domains after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1595 * * @param object[] $items The array of found domain objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -664,7 +664,7 @@ * Filters a single domain item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2068 * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -676,7 +676,7 @@ * Filters the FOUND_ROWS() query for domains. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:640 * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -693,7 +693,7 @@ * column for a event row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous author id value. * @param int $new_value The new author id value. @@ -708,7 +708,7 @@ * column for a event row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous object id value. * @param int $new_value The new object id value. @@ -720,7 +720,7 @@ * Fires before events are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:883 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -730,7 +730,7 @@ * Fires after events query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1159 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -740,7 +740,7 @@ * Filters the SQL clauses for a events query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:994 * * @param array $clauses { * Associative array of SQL clause strings. @@ -761,7 +761,7 @@ * Filters the columns to search when performing a events search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1306 * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -774,7 +774,7 @@ * Filters the found events after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1595 * * @param object[] $items The array of found event objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -786,7 +786,7 @@ * Filters a single event item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2068 * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -798,7 +798,7 @@ * Filters the FOUND_ROWS() query for events. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:640 * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -815,7 +815,7 @@ * column for a form row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous name value. * @param string $new_value The new name value. @@ -830,7 +830,7 @@ * column for a form row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous slug value. * @param string $new_value The new slug value. @@ -845,7 +845,7 @@ * column for a form row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous active value. * @param int $new_value The new active value. @@ -860,7 +860,7 @@ * column for a form row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous settings value. * @param string $new_value The new settings value. @@ -872,7 +872,7 @@ * Fires before forms are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:883 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -882,7 +882,7 @@ * Fires after forms query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1159 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -892,7 +892,7 @@ * Filters the SQL clauses for a forms query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:994 * * @param array $clauses { * Associative array of SQL clause strings. @@ -913,7 +913,7 @@ * Filters the columns to search when performing a forms search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1306 * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -926,7 +926,7 @@ * Filters the found forms after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1595 * * @param object[] $items The array of found form objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -938,7 +938,7 @@ * Filters a single form item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2068 * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -950,7 +950,7 @@ * Filters the FOUND_ROWS() query for forms. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:640 * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -967,7 +967,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous plan id value. * @param int $new_value The new plan id value. @@ -982,7 +982,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous initial amount value. * @param string $new_value The new initial amount value. @@ -997,7 +997,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous recurring value. * @param int $new_value The new recurring value. @@ -1012,7 +1012,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous auto renew value. * @param int $new_value The new auto renew value. @@ -1027,7 +1027,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous duration value. * @param int $new_value The new duration value. @@ -1042,7 +1042,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous amount value. * @param string $new_value The new amount value. @@ -1057,7 +1057,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous date expiration value. * @param string $new_value The new date expiration value. @@ -1072,7 +1072,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous date payment plan completed value. * @param string $new_value The new date payment plan completed value. @@ -1087,7 +1087,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous times billed value. * @param int $new_value The new times billed value. @@ -1102,7 +1102,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous status value. * @param string $new_value The new status value. @@ -1117,7 +1117,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous gateway customer id value. * @param string $new_value The new gateway customer id value. @@ -1132,7 +1132,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous gateway subscription id value. * @param string $new_value The new gateway subscription id value. @@ -1144,7 +1144,7 @@ * Fires before memberships are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:883 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1154,7 +1154,7 @@ * Fires after memberships query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1159 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1164,7 +1164,7 @@ * Filters the SQL clauses for a memberships query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:994 * * @param array $clauses { * Associative array of SQL clause strings. @@ -1185,7 +1185,7 @@ * Filters the columns to search when performing a memberships search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1306 * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -1198,7 +1198,7 @@ * Filters the found memberships after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1595 * * @param object[] $items The array of found membership objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1210,7 +1210,7 @@ * Filters a single membership item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2068 * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1222,7 +1222,7 @@ * Filters the FOUND_ROWS() query for memberships. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:640 * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1239,7 +1239,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous status value. * @param string $new_value The new status value. @@ -1254,7 +1254,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous customer id value. * @param int $new_value The new customer id value. @@ -1269,7 +1269,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous membership id value. * @param int $new_value The new membership id value. @@ -1284,7 +1284,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous parent id value. * @param int $new_value The new parent id value. @@ -1299,7 +1299,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous product id value. * @param int $new_value The new product id value. @@ -1314,7 +1314,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous subtotal value. * @param string $new_value The new subtotal value. @@ -1329,7 +1329,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous refund total value. * @param string $new_value The new refund total value. @@ -1344,7 +1344,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous tax total value. * @param string $new_value The new tax total value. @@ -1359,7 +1359,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous total value. * @param string $new_value The new total value. @@ -1371,7 +1371,7 @@ * Fires before payments are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:883 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1381,7 +1381,7 @@ * Fires after payments query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1159 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1391,7 +1391,7 @@ * Filters the SQL clauses for a payments query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:994 * * @param array $clauses { * Associative array of SQL clause strings. @@ -1412,7 +1412,7 @@ * Filters the columns to search when performing a payments search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1306 * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -1425,7 +1425,7 @@ * Filters the found payments after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1595 * * @param object[] $items The array of found payment objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1437,7 +1437,7 @@ * Filters a single payment item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2068 * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1449,7 +1449,7 @@ * Filters the FOUND_ROWS() query for payments. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:640 * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1463,7 +1463,7 @@ * Fires before posts are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:883 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1473,7 +1473,7 @@ * Fires after posts query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1159 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1483,7 +1483,7 @@ * Filters the SQL clauses for a posts query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:994 * * @param array $clauses { * Associative array of SQL clause strings. @@ -1504,7 +1504,7 @@ * Filters the columns to search when performing a posts search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1306 * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -1517,7 +1517,7 @@ * Filters the found posts after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1595 * * @param object[] $items The array of found post objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1529,7 +1529,7 @@ * Filters a single post item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2068 * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1541,7 +1541,7 @@ * Filters the FOUND_ROWS() query for posts. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:640 * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1558,7 +1558,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous parent id value. * @param int $new_value The new parent id value. @@ -1573,7 +1573,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous amount value. * @param string $new_value The new amount value. @@ -1588,7 +1588,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param string $old_value The previous setup fee value. * @param string $new_value The new setup fee value. @@ -1603,7 +1603,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous recurring value. * @param int $new_value The new recurring value. @@ -1618,7 +1618,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous trial duration value. * @param int $new_value The new trial duration value. @@ -1633,7 +1633,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous duration value. * @param int $new_value The new duration value. @@ -1648,7 +1648,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous billing cycles value. * @param int $new_value The new billing cycles value. @@ -1663,7 +1663,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous list order value. * @param int $new_value The new list order value. @@ -1678,7 +1678,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous active value. * @param int $new_value The new active value. @@ -1690,7 +1690,7 @@ * Fires before products are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:883 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1700,7 +1700,7 @@ * Fires after products query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1159 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1710,7 +1710,7 @@ * Filters the SQL clauses for a products query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:994 * * @param array $clauses { * Associative array of SQL clause strings. @@ -1731,7 +1731,7 @@ * Filters the columns to search when performing a products search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1306 * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -1744,7 +1744,7 @@ * Filters the found products after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1595 * * @param object[] $items The array of found product objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1756,7 +1756,7 @@ * Filters a single product item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2068 * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1768,7 +1768,7 @@ * Filters the FOUND_ROWS() query for products. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:640 * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1785,7 +1785,7 @@ * column for a webhook row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous active value. * @param int $new_value The new active value. @@ -1800,7 +1800,7 @@ * column for a webhook row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2303 * * @param int $old_value The previous hidden value. * @param int $new_value The new hidden value. @@ -1812,7 +1812,7 @@ * Fires before webhooks are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:883 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1822,7 +1822,7 @@ * Fires after webhooks query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1159 * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1832,7 +1832,7 @@ * Filters the SQL clauses for a webhooks query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:994 * * @param array $clauses { * Associative array of SQL clause strings. @@ -1853,7 +1853,7 @@ * Filters the columns to search when performing a webhooks search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1306 * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -1866,7 +1866,7 @@ * Filters the found webhooks after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:1595 * * @param object[] $items The array of found webhook objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1878,7 +1878,7 @@ * Filters a single webhook item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:2068 * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1890,7 +1890,7 @@ * Filters the FOUND_ROWS() query for webhooks. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php + * @see vendor/berlindb/core/src/Database/Query.php:640 * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). diff --git a/inc/documentation/generate-berlindb-hooks.php b/inc/documentation/generate-berlindb-hooks.php index 79959aab..9f0b33ca 100644 --- a/inc/documentation/generate-berlindb-hooks.php +++ b/inc/documentation/generate-berlindb-hooks.php @@ -344,6 +344,18 @@ function ($m) { $prefix = 'wu'; $src = $query_php_relative; +// Build a map of hook-key → line number inside Query.php for @see tags. +$hook_lines = [ + 'transition' => find_line_number($query_php_absolute, '/do_action\(\s*\$key_action/'), + 'pre_get' => find_line_number($query_php_absolute, '/pre_get_.*item_name_plural/'), + 'parse_query' => find_line_number($query_php_absolute, '/parse_.*item_name_plural.*_query/'), + 'query_clauses' => find_line_number($query_php_absolute, '/item_name_plural.*_query_clauses/'), + 'search_columns' => find_line_number($query_php_absolute, '/item_name_plural.*_search_columns/'), + 'the_items' => find_line_number($query_php_absolute, '/the_.*item_name_plural/'), + 'filter_item' => find_line_number($query_php_absolute, '/filter_.*item_name.*_item/'), + 'found_query' => find_line_number($query_php_absolute, '/found_.*item_name_plural.*_query/'), +]; + ob_start(); echo "getMessage())); } } diff --git a/inc/integrations/providers/closte/class-closte-domain-mapping.php b/inc/integrations/providers/closte/class-closte-domain-mapping.php index 70813e8e..feec425c 100644 --- a/inc/integrations/providers/closte/class-closte-domain-mapping.php +++ b/inc/integrations/providers/closte/class-closte-domain-mapping.php @@ -55,9 +55,9 @@ public function get_explainer_lines(): array { return [ 'will' => [ - /* translators: %s is the hosting provider name (e.g. Closte) */ + // translators: %s: hosting provider name. sprintf(__('Send API calls to %s servers with domain names added to this network', 'ultimate-multisite'), 'Closte'), - /* translators: %s is the hosting provider name (e.g. Closte) */ + // translators: %s: hosting provider name. sprintf(__('Fetch and install a SSL certificate on %s platform after the domain is added.', 'ultimate-multisite'), 'Closte'), ], 'will_not' => [], diff --git a/inc/integrations/providers/serverpilot/class-serverpilot-domain-mapping.php b/inc/integrations/providers/serverpilot/class-serverpilot-domain-mapping.php index 0b7ac9cb..6f6ca705 100644 --- a/inc/integrations/providers/serverpilot/class-serverpilot-domain-mapping.php +++ b/inc/integrations/providers/serverpilot/class-serverpilot-domain-mapping.php @@ -56,9 +56,9 @@ public function get_explainer_lines(): array { return [ 'will' => [ - /* translators: %s is the hosting provider name */ + // translators: %s: hosting provider name. sprintf(__('Send API calls to %s servers with domain names added to this network', 'ultimate-multisite'), 'ServerPilot'), - /* translators: %s is the hosting provider name */ + // translators: %s: hosting provider name. sprintf(__('Fetch and install a SSL certificate on %s platform after the domain is added.', 'ultimate-multisite'), 'ServerPilot'), ], 'will_not' => [], diff --git a/inc/list-tables/class-customers-membership-list-table.php b/inc/list-tables/class-customers-membership-list-table.php index 9c60dea1..e83454a1 100644 --- a/inc/list-tables/class-customers-membership-list-table.php +++ b/inc/list-tables/class-customers-membership-list-table.php @@ -50,8 +50,17 @@ public function column_responsive($item): void { $product_count = 1 + count($item->get_addon_ids()); - // translators: %s is the product name, %2$s is the count of other products. - $products_list = $p ? sprintf(_n('Contains %1$s', 'Contains %1$s and %2$s other product(s)', $product_count, 'ultimate-multisite'), $p->get_name(), count($item->get_addon_ids())) : ''; // phpcs:ignore + $addon_count = count($item->get_addon_ids()); + + if ( ! $p) { + $products_list = ''; + } elseif ($addon_count === 0) { + // translators: %s: the product name. + $products_list = sprintf(__('Contains %s', 'ultimate-multisite'), $p->get_name()); + } else { + // translators: %1$s: the product name, %2$s: the count of other products. + $products_list = sprintf(__('Contains %1$s and %2$s other product(s)', 'ultimate-multisite'), $p->get_name(), $addon_count); + } wu_responsive_table_row( [ diff --git a/inc/list-tables/class-membership-list-table-widget.php b/inc/list-tables/class-membership-list-table-widget.php index 829bac26..4b69f0ec 100644 --- a/inc/list-tables/class-membership-list-table-widget.php +++ b/inc/list-tables/class-membership-list-table-widget.php @@ -171,12 +171,16 @@ public function column_amount($item) { if ($item->is_recurring()) { $duration = $item->get_duration(); - $message = sprintf( - // translators: %1$s the duration, and %2$s the duration unit (day, week, month, etc) - _n('every %2$s', 'every %1$s %2$s', $duration, 'ultimate-multisite'), // phpcs:ignore - $duration, - $item->get_duration_unit() - ); + if ($duration <= 1) { + $message = $item->get_duration_unit(); + } else { + $message = sprintf( + // translators: %1$s: the duration number, %2$s: the duration unit (days, weeks, months, etc). + __('every %1$s %2$s', 'ultimate-multisite'), + $duration, + $item->get_duration_unit() + ); + } if ( ! $item->is_forever_recurring()) { $billing_cycles_message = sprintf( diff --git a/inc/list-tables/class-membership-list-table.php b/inc/list-tables/class-membership-list-table.php index f0f7cad3..e78475ed 100644 --- a/inc/list-tables/class-membership-list-table.php +++ b/inc/list-tables/class-membership-list-table.php @@ -134,12 +134,16 @@ public function column_amount($item) { $duration = $item->get_duration(); - $message = sprintf( - // translators: %1$s the duration, and %2$s the duration unit (day, week, month, etc) - _n('every %2$s', 'every %1$s %2$s', $duration, 'ultimate-multisite'), // phpcs:ignore - $duration, - $item->get_duration_unit() - ); + if ($duration <= 1) { + $message = $item->get_duration_unit(); + } else { + $message = sprintf( + // translators: %1$s: the duration number, %2$s: the duration unit (days, weeks, months, etc). + __('every %1$s %2$s', 'ultimate-multisite'), + $duration, + $item->get_duration_unit() + ); + } if ( ! $item->is_forever_recurring()) { $billing_cycles_message = sprintf( diff --git a/inc/list-tables/class-product-list-table.php b/inc/list-tables/class-product-list-table.php index 01201992..ae295161 100644 --- a/inc/list-tables/class-product-list-table.php +++ b/inc/list-tables/class-product-list-table.php @@ -141,12 +141,16 @@ public function column_amount($item) { if ($item->is_recurring()) { $duration = $item->get_duration(); - $message = sprintf( - // translators: %1$s the duration, and %2$s the duration unit (day, week, month, etc) - _n('every %2$s', 'every %1$s %2$s', $duration, 'ultimate-multisite'), // phpcs:ignore - $duration, - $item->get_duration_unit() - ); + if ($duration <= 1) { + $message = $item->get_duration_unit(); + } else { + $message = sprintf( + // translators: %1$s: the duration number, %2$s: the duration unit (days, weeks, months, etc). + __('every %1$s %2$s', 'ultimate-multisite'), + $duration, + $item->get_duration_unit() + ); + } if ( ! $item->is_forever_recurring()) { $billing_cycles_message = sprintf( diff --git a/inc/models/class-checkout-form.php b/inc/models/class-checkout-form.php index 4f40cc6c..eb754c28 100644 --- a/inc/models/class-checkout-form.php +++ b/inc/models/class-checkout-form.php @@ -1316,12 +1316,12 @@ public static function membership_change_form_fields() { foreach ($products as $product) { $days_in_cycle = (int) wu_get_days_in_cycle($product->get_duration_unit(), $product->get_duration()); - $label = sprintf( - // translators: %1$s the duration, and %2$s the duration unit (day, week, month, etc) - _n('%2$s', '%1$s %2$s', $product->get_duration(), 'ultimate-multisite'), // phpcs:ignore - $product->get_duration(), - wu_get_translatable_string($product->get_duration() <= 1 ? $product->get_duration_unit() : $product->get_duration_unit() . 's') - ); + $product_duration = $product->get_duration(); + $product_unit = wu_get_translatable_string($product_duration <= 1 ? $product->get_duration_unit() : $product->get_duration_unit() . 's'); + + $label = $product_duration <= 1 + ? $product_unit + : $product_duration . ' ' . $product_unit; $period_selection[ $days_in_cycle ] = [ 'duration' => $product->get_duration(), @@ -1347,12 +1347,12 @@ public static function membership_change_form_fields() { */ $days_in_cycle = (int) wu_get_days_in_cycle($variation['duration_unit'], $variation['duration']); - $label = sprintf( - // translators: %1$s the duration, and %2$s the duration unit (day, week, month, etc) - _n('%2$s', '%1$s %2$s', $variation['duration'], 'ultimate-multisite'), // phpcs:ignore - $variation['duration'], - wu_get_translatable_string($variation['duration'] <= 1 ? $variation['duration_unit'] : $variation['duration_unit'] . 's') - ); + $var_duration = $variation['duration']; + $var_unit = wu_get_translatable_string($var_duration <= 1 ? $variation['duration_unit'] : $variation['duration_unit'] . 's'); + + $label = $var_duration <= 1 + ? $var_unit + : $var_duration . ' ' . $var_unit; $period_selection[ $days_in_cycle ] = [ 'duration' => $variation['duration'], diff --git a/inc/models/class-membership.php b/inc/models/class-membership.php index b220122e..e1b3eca8 100644 --- a/inc/models/class-membership.php +++ b/inc/models/class-membership.php @@ -869,14 +869,19 @@ public function delete_scheduled_swap(): void { */ public function get_recurring_description() { - $description = sprintf( - // translators: %1$s the duration, and %2$s the duration unit (day, week, month, etc) - _n('every %2$s', 'every %1$s %2$s', $this->get_duration(), 'ultimate-multisite'), // phpcs:ignore - $this->get_duration(), - wu_get_translatable_string(($this->get_duration() <= 1 ? $this->get_duration_unit() : $this->get_duration_unit() . 's')) - ); + $duration = $this->get_duration(); + $duration_unit = wu_get_translatable_string($duration <= 1 ? $this->get_duration_unit() : $this->get_duration_unit() . 's'); + + if ($duration <= 1) { + return $duration_unit; + } - return $description; + return sprintf( + // translators: %1$s: the duration number, %2$s: the duration unit (days, weeks, months, etc). + __('every %1$s %2$s', 'ultimate-multisite'), + $duration, + $duration_unit + ); } /** @@ -910,13 +915,25 @@ public function get_price_description(): string { if ($this->is_recurring()) { $duration = $this->get_duration(); - $message = sprintf( - // translators: %1$s is the formatted price, %2$s the duration, and %3$s the duration unit (day, week, month, etc) - _n('%1$s every %3$s', '%1$s every %2$s %3$s', $duration, 'ultimate-multisite'), // phpcs:ignore - wu_format_currency($this->get_amount(), $this->get_currency()), - $duration, - wu_get_translatable_string($duration <= 1 ? $this->get_duration_unit() : $this->get_duration_unit() . 's') - ); + $formatted_price = wu_format_currency($this->get_amount(), $this->get_currency()); + $duration_unit = wu_get_translatable_string($duration <= 1 ? $this->get_duration_unit() : $this->get_duration_unit() . 's'); + + if ($duration <= 1) { + $message = sprintf( + // translators: %1$s: the formatted price, %2$s: the duration unit (day, week, month, etc). + __('%1$s / %2$s', 'ultimate-multisite'), + $formatted_price, + $duration_unit + ); + } else { + $message = sprintf( + // translators: %1$s: the formatted price, %2$s: the duration number, %3$s: the duration unit (days, weeks, months, etc). + __('%1$s every %2$s %3$s', 'ultimate-multisite'), + $formatted_price, + $duration, + $duration_unit + ); + } $pricing['subscription'] = $message; diff --git a/inc/models/class-product.php b/inc/models/class-product.php index 2960dfbf..24019711 100644 --- a/inc/models/class-product.php +++ b/inc/models/class-product.php @@ -842,13 +842,25 @@ public function get_price_description($include_fees = true) { if ($this->is_recurring()) { $duration = $this->get_duration(); - $message = sprintf( - // translators: %1$s is the formatted price, %2$s the duration, and %3$s the duration unit (day, week, month, etc) - _n('%1$s every %3$s', '%1$s every %2$s %3$s', $duration, 'ultimate-multisite'), // phpcs:ignore - wu_format_currency($this->get_amount(), $this->get_currency()), - $duration, - wu_get_translatable_string($duration <= 1 ? $this->get_duration_unit() : $this->get_duration_unit() . 's') - ); + $formatted_price = wu_format_currency($this->get_amount(), $this->get_currency()); + $duration_unit = wu_get_translatable_string($duration <= 1 ? $this->get_duration_unit() : $this->get_duration_unit() . 's'); + + if ($duration <= 1) { + $message = sprintf( + // translators: %1$s: the formatted price, %2$s: the duration unit (day, week, month, etc). + __('%1$s / %2$s', 'ultimate-multisite'), + $formatted_price, + $duration_unit + ); + } else { + $message = sprintf( + // translators: %1$s: the formatted price, %2$s: the duration number, %3$s: the duration unit (days, weeks, months, etc). + __('%1$s every %2$s %3$s', 'ultimate-multisite'), + $formatted_price, + $duration, + $duration_unit + ); + } $pricing['subscription'] = $message; @@ -896,14 +908,19 @@ public function get_recurring_description() { return __('one-time payment', 'ultimate-multisite'); } - $description = sprintf( - // translators: %1$s the duration, and %2$s the duration unit (day, week, month, etc) - _n('every %2$s', 'every %1$s %2$s', $this->get_duration(), 'ultimate-multisite'), // phpcs:ignore - $this->get_duration(), - wu_get_translatable_string($this->get_duration() <= 1 ? $this->get_duration_unit() : $this->get_duration_unit() . 's') - ); + $duration = $this->get_duration(); + $duration_unit = wu_get_translatable_string($duration <= 1 ? $this->get_duration_unit() : $this->get_duration_unit() . 's'); + + if ($duration <= 1) { + return $duration_unit; + } - return $description; + return sprintf( + // translators: %1$s: the duration number, %2$s: the duration unit (days, weeks, months, etc). + __('every %1$s %2$s', 'ultimate-multisite'), + $duration, + $duration_unit + ); } /** diff --git a/lang/ultimate-multisite.pot b/lang/ultimate-multisite.pot index 794a126d..69b90ce4 100644 --- a/lang/ultimate-multisite.pot +++ b/lang/ultimate-multisite.pot @@ -9,7 +9,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-02-15T06:27:37+00:00\n" +"POT-Creation-Date: 2026-02-17T01:47:30+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: ultimate-multisite\n" @@ -226,7 +226,7 @@ msgstr "" #: inc/admin-pages/class-payment-edit-admin-page.php:653 #: inc/list-tables/class-broadcast-list-table.php:417 #: inc/list-tables/class-event-list-table.php:213 -#: inc/list-tables/class-product-list-table.php:254 +#: inc/list-tables/class-product-list-table.php:258 #: inc/list-tables/class-site-list-table.php:272 #: views/base/checkout-forms/js-templates.php:131 #: views/domain/dns-table.php:16 @@ -270,9 +270,9 @@ msgstr "" #: inc/admin-pages/class-payment-edit-admin-page.php:1043 #: inc/admin-pages/class-payment-list-admin-page.php:105 #: inc/admin-pages/class-payment-list-admin-page.php:106 -#: inc/list-tables/class-membership-list-table-widget.php:263 -#: inc/list-tables/class-membership-list-table.php:177 -#: inc/list-tables/class-membership-list-table.php:223 +#: inc/list-tables/class-membership-list-table-widget.php:267 +#: inc/list-tables/class-membership-list-table.php:181 +#: inc/list-tables/class-membership-list-table.php:227 #: inc/list-tables/class-payment-list-table.php:196 #: inc/list-tables/class-payment-list-table.php:221 #: inc/list-tables/customer-panel/class-invoice-list-table.php:34 @@ -315,7 +315,7 @@ msgstr "" #: inc/admin-pages/class-broadcast-edit-admin-page.php:195 #: inc/list-tables/class-base-list-table.php:981 #: inc/list-tables/class-broadcast-list-table.php:151 -#: inc/list-tables/class-membership-list-table-widget.php:210 +#: inc/list-tables/class-membership-list-table-widget.php:214 #: inc/list-tables/class-payment-list-table-widget.php:155 msgid "No customer found" msgstr "" @@ -753,7 +753,7 @@ msgstr "" #: inc/admin-pages/class-checkout-form-edit-admin-page.php:1154 #: inc/admin-pages/class-customer-edit-admin-page.php:598 #: inc/admin-pages/class-email-edit-admin-page.php:158 -#: inc/list-tables/class-product-list-table.php:255 +#: inc/list-tables/class-product-list-table.php:259 #: inc/ui/class-checkout-element.php:159 #: views/base/checkout-forms/js-templates.php:135 msgid "Slug" @@ -861,7 +861,7 @@ msgstr "" #: inc/list-tables/class-domain-list-table.php:164 #: inc/list-tables/class-domain-list-table.php:187 #: inc/list-tables/class-domain-list-table.php:190 -#: inc/list-tables/class-membership-list-table.php:275 +#: inc/list-tables/class-membership-list-table.php:279 #: inc/list-tables/class-webhook-list-table.php:178 #: views/emails/admin/domain-created.php:36 msgid "Active" @@ -1383,7 +1383,7 @@ msgstr "" #: inc/admin-pages/class-customer-edit-admin-page.php:881 #: inc/list-tables/class-product-list-table.php:132 -#: inc/list-tables/class-product-list-table.php:178 +#: inc/list-tables/class-product-list-table.php:182 #: inc/models/class-payment.php:572 #: inc/models/class-payment.php:578 #: views/admin-pages/fields/field-text-display.php:47 @@ -1412,7 +1412,7 @@ msgstr "" #: inc/installers/class-multisite-network-installer.php:79 #: inc/installers/class-recommended-plugins-installer.php:49 #: inc/installers/class-recommended-plugins-installer.php:60 -#: inc/list-tables/class-membership-list-table.php:287 +#: inc/list-tables/class-membership-list-table.php:291 #: inc/list-tables/class-payment-list-table.php:223 #: inc/list-tables/class-payment-list-table.php:281 #: inc/list-tables/class-site-list-table.php:361 @@ -1538,7 +1538,7 @@ msgstr "" #: inc/models/class-checkout-form.php:579 #: inc/models/class-checkout-form.php:736 #: inc/ui/class-login-form-element.php:230 -#: views/checkout/partials/inline-login-prompt.php:31 +#: views/checkout/partials/inline-login-prompt.php:23 #: views/wizards/host-integrations/cpanel-instructions.php:12 msgid "Password" msgstr "" @@ -1786,7 +1786,7 @@ msgstr "" #: inc/admin-pages/class-discount-code-edit-admin-page.php:184 #: inc/admin-pages/class-membership-list-admin-page.php:152 -#: inc/list-tables/class-membership-list-table.php:242 +#: inc/list-tables/class-membership-list-table.php:246 msgid "Expiration Date" msgstr "" @@ -2320,9 +2320,9 @@ msgid "Timestamps" msgstr "" #: inc/admin-pages/class-edit-admin-page.php:355 -#: inc/list-tables/class-customers-membership-list-table.php:94 +#: inc/list-tables/class-customers-membership-list-table.php:103 #: inc/list-tables/class-event-list-table.php:214 -#: inc/list-tables/class-membership-list-table.php:181 +#: inc/list-tables/class-membership-list-table.php:185 #: inc/list-tables/class-payment-list-table-widget.php:222 #: inc/list-tables/class-payment-list-table.php:200 #: inc/list-tables/customer-panel/class-invoice-list-table.php:36 @@ -2401,8 +2401,8 @@ msgstr "" #: inc/admin-pages/class-membership-edit-admin-page.php:510 #: inc/admin-pages/class-membership-list-admin-page.php:91 #: inc/list-tables/class-customer-list-table.php:46 -#: inc/list-tables/class-membership-list-table-widget.php:264 -#: inc/list-tables/class-membership-list-table.php:178 +#: inc/list-tables/class-membership-list-table-widget.php:268 +#: inc/list-tables/class-membership-list-table.php:182 #: inc/list-tables/class-payment-list-table-widget.php:220 #: inc/list-tables/class-payment-list-table.php:197 #: inc/list-tables/class-site-list-table.php:273 @@ -3064,12 +3064,12 @@ msgstr "" #: inc/admin-pages/class-product-list-admin-page.php:98 #: inc/admin-pages/class-product-list-admin-page.php:109 #: inc/admin-pages/class-top-admin-nav-menu.php:103 -#: inc/checkout/class-checkout.php:2194 +#: inc/checkout/class-checkout.php:2195 #: inc/checkout/signup-fields/class-signup-field-pricing-table.php:170 #: inc/checkout/signup-fields/class-signup-field-products.php:149 #: inc/checkout/signup-fields/class-signup-field-products.php:150 #: inc/debug/class-debug.php:178 -#: inc/list-tables/class-customers-membership-list-table.php:76 +#: inc/list-tables/class-customers-membership-list-table.php:85 #: inc/list-tables/class-product-list-table.php:40 #: views/checkout/register.php:17 #: views/emails/admin/payment-received.php:20 @@ -3218,7 +3218,7 @@ msgstr "" #: inc/admin-pages/class-membership-edit-admin-page.php:774 #: inc/admin-pages/class-payment-edit-admin-page.php:1086 -#: inc/list-tables/class-customers-membership-list-table.php:81 +#: inc/list-tables/class-customers-membership-list-table.php:90 #: inc/list-tables/class-customers-payment-list-table.php:67 #: inc/list-tables/class-payment-list-table.php:235 msgid "Gateway" @@ -3328,7 +3328,7 @@ msgstr "" #: inc/admin-pages/class-payment-edit-admin-page.php:680 #: inc/checkout/signup-fields/class-signup-field-order-bump.php:169 #: inc/checkout/signup-fields/class-signup-field-products.php:56 -#: inc/list-tables/class-membership-list-table.php:179 +#: inc/list-tables/class-membership-list-table.php:183 #: inc/list-tables/class-product-list-table.php:39 #: views/checkout/paypal/confirm.php:94 #: views/dashboard-statistics/widget-revenue.php:70 @@ -3368,7 +3368,7 @@ msgstr "" #: inc/admin-pages/class-membership-edit-admin-page.php:1363 #: inc/admin-pages/class-membership-edit-admin-page.php:1508 -#: inc/list-tables/class-product-list-table.php:204 +#: inc/list-tables/class-product-list-table.php:208 #: inc/ui/class-current-membership-element.php:527 msgid "Product not found." msgstr "" @@ -3471,7 +3471,7 @@ msgid "The membership status." msgstr "" #: inc/admin-pages/class-membership-list-admin-page.php:144 -#: inc/list-tables/class-membership-list-table.php:202 +#: inc/list-tables/class-membership-list-table.php:206 msgid "Lifetime" msgstr "" @@ -3538,8 +3538,8 @@ msgstr "" #: inc/admin-pages/class-payment-edit-admin-page.php:230 #: inc/admin-pages/class-payment-edit-admin-page.php:389 -#: inc/gateways/class-base-stripe-gateway.php:3546 -#: inc/managers/class-gateway-manager.php:607 +#: inc/gateways/class-base-stripe-gateway.php:3553 +#: inc/managers/class-gateway-manager.php:609 msgid "Payment not found." msgstr "" @@ -4095,9 +4095,9 @@ msgstr "" #: inc/admin-pages/class-product-edit-admin-page.php:433 #: inc/admin-pages/class-product-edit-admin-page.php:434 #: inc/admin-pages/class-product-edit-admin-page.php:449 -#: inc/list-tables/class-membership-list-table-widget.php:265 -#: inc/list-tables/class-membership-list-table.php:180 -#: inc/list-tables/class-product-list-table.php:256 +#: inc/list-tables/class-membership-list-table-widget.php:269 +#: inc/list-tables/class-membership-list-table.php:184 +#: inc/list-tables/class-product-list-table.php:260 #: views/invoice/template.php:107 msgid "Price" msgstr "" @@ -4123,7 +4123,7 @@ msgid "Check if you want to add a setup fee." msgstr "" #: inc/admin-pages/class-product-edit-admin-page.php:573 -#: inc/list-tables/class-product-list-table.php:257 +#: inc/list-tables/class-product-list-table.php:261 msgid "Setup Fee" msgstr "" @@ -4620,7 +4620,7 @@ msgid "Default Content" msgstr "" #: inc/admin-pages/class-setup-wizard-admin-page.php:465 -msgid "Starting from scratch can be scarry, specially when first starting out. In this step, you can create default content to have a starting point for your network. Everything can be customized later." +msgid "Starting from scratch can be scary, especially when first starting out. In this step, you can create default content to have a starting point for your network. Everything can be customized later." msgstr "" #: inc/admin-pages/class-setup-wizard-admin-page.php:478 @@ -4936,7 +4936,7 @@ msgstr "" #: inc/admin-pages/class-site-list-admin-page.php:315 #: inc/list-tables/class-checkout-form-list-table.php:158 #: inc/list-tables/class-email-list-table.php:230 -#: inc/list-tables/class-product-list-table.php:212 +#: inc/list-tables/class-product-list-table.php:216 #, php-format msgid "Copy of %s" msgstr "" @@ -6983,7 +6983,7 @@ msgstr "" #. translators: 1. Object class name; 2. Set method name #: inc/apis/trait-rest-api.php:266 -#: inc/apis/trait-wp-cli.php:340 +#: inc/apis/trait-wp-cli.php:397 #, php-format msgid "The %1$s object does not have a %2$s method" msgstr "" @@ -6992,23 +6992,63 @@ msgstr "" msgid "Something went wrong (Code 2)." msgstr "" -#: inc/apis/trait-wp-cli.php:121 +#: inc/apis/trait-wp-cli.php:79 +msgid "Ultimate Multisite commands." +msgstr "" + +#. translators: %s: the entity label, e.g. "customer" or "checkout form" +#: inc/apis/trait-wp-cli.php:95 +#, php-format +msgid "Manages %ss." +msgstr "" + +#. translators: %s: the entity label, e.g. "customer" or "checkout form" +#: inc/apis/trait-wp-cli.php:130 +#, php-format +msgid "Gets a %s by ID." +msgstr "" + +#. translators: %s: the entity label, e.g. "customer" or "checkout form" +#: inc/apis/trait-wp-cli.php:135 +#, php-format +msgid "Lists %ss." +msgstr "" + +#. translators: %s: the entity label, e.g. "customer" or "checkout form" +#: inc/apis/trait-wp-cli.php:140 +#, php-format +msgid "Creates a new %s." +msgstr "" + +#. translators: %s: the entity label, e.g. "customer" or "checkout form" +#: inc/apis/trait-wp-cli.php:145 +#, php-format +msgid "Updates an existing %s." +msgstr "" + +#. translators: %s: the entity label, e.g. "customer" or "checkout form" +#: inc/apis/trait-wp-cli.php:150 +#, php-format +msgid "Deletes an existing %s." +msgstr "" + +#: inc/apis/trait-wp-cli.php:178 msgid "The id for the resource." msgstr "" -#: inc/apis/trait-wp-cli.php:136 +#: inc/apis/trait-wp-cli.php:193 msgid "No description found." msgstr "" -#: inc/apis/trait-wp-cli.php:155 +#: inc/apis/trait-wp-cli.php:212 msgid "Output just the id when the operation is successful." msgstr "" -#: inc/apis/trait-wp-cli.php:164 +#: inc/apis/trait-wp-cli.php:221 msgid "Render response in a particular format." msgstr "" -#: inc/apis/trait-wp-cli.php:180 +#: inc/apis/trait-wp-cli.php:237 msgid "Limit response to specific fields. Defaults to id, name" msgstr "" @@ -7030,7 +7070,7 @@ msgid "The payment in question has an invalid status." msgstr "" #: inc/checkout/class-cart.php:800 -#: inc/gateways/class-base-stripe-gateway.php:767 +#: inc/gateways/class-base-stripe-gateway.php:773 msgid "You are not allowed to modify this membership." msgstr "" @@ -7122,14 +7162,20 @@ msgstr "" msgid "The amount for %1$s must be at least %2$s." msgstr "" +#. translators: %1$s is the product name, %2$s is the maximum amount formatted as currency +#: inc/checkout/class-cart.php:1790 +#, php-format +msgid "The amount for %1$s cannot exceed %2$s." +msgstr "" + #. translators: placeholder is the product name. -#: inc/checkout/class-cart.php:1864 +#: inc/checkout/class-cart.php:1887 #, php-format msgid "Signup Fee for %s" msgstr "" #. translators: placeholder is the product name. -#: inc/checkout/class-cart.php:1864 +#: inc/checkout/class-cart.php:1887 #, php-format msgid "Signup Credit for %s" msgstr "" @@ -7161,50 +7207,50 @@ msgstr "" msgid "Resend verification email →" msgstr "" -#: inc/checkout/class-checkout-pages.php:695 +#: inc/checkout/class-checkout-pages.php:693 msgid "Ultimate Multisite - Register Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:696 +#: inc/checkout/class-checkout-pages.php:694 msgid "Ultimate Multisite - Login Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:697 +#: inc/checkout/class-checkout-pages.php:695 msgid "Ultimate Multisite - Site Blocked Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:698 +#: inc/checkout/class-checkout-pages.php:696 msgid "Ultimate Multisite - Membership Update Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:699 +#: inc/checkout/class-checkout-pages.php:697 msgid "Ultimate Multisite - New Site Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:794 +#: inc/checkout/class-checkout-pages.php:793 msgid "Payment confirmed! Refreshing page..." msgstr "" -#: inc/checkout/class-checkout-pages.php:795 +#: inc/checkout/class-checkout-pages.php:794 msgid "Verifying your payment with Stripe..." msgstr "" -#: inc/checkout/class-checkout-pages.php:796 +#: inc/checkout/class-checkout-pages.php:795 msgid "Payment verification is taking longer than expected. Your payment may still be processing. Please refresh the page or contact support if you believe payment was made." msgstr "" -#: inc/checkout/class-checkout-pages.php:797 +#: inc/checkout/class-checkout-pages.php:796 msgid "Error checking payment status. Retrying..." msgstr "" -#: inc/checkout/class-checkout-pages.php:798 +#: inc/checkout/class-checkout-pages.php:797 msgid "Checking payment status..." msgstr "" #: inc/checkout/class-checkout.php:754 #: inc/checkout/class-checkout.php:762 -#: inc/checkout/class-checkout.php:2418 -#: inc/checkout/class-checkout.php:2424 +#: inc/checkout/class-checkout.php:2419 +#: inc/checkout/class-checkout.php:2425 msgid "Payment gateway not registered." msgstr "" @@ -7266,12 +7312,12 @@ msgid "Already have an account?" msgstr "" #: inc/checkout/class-checkout.php:1834 -#: views/checkout/partials/inline-login-prompt.php:59 +#: views/checkout/partials/inline-login-prompt.php:51 msgid "Sign in" msgstr "" #: inc/checkout/class-checkout.php:1835 -#: views/checkout/partials/inline-login-prompt.php:51 +#: views/checkout/partials/inline-login-prompt.php:43 msgid "Forgot password?" msgstr "" @@ -7281,34 +7327,39 @@ msgstr "" msgid "Cancel" msgstr "" -#: inc/checkout/class-checkout.php:2190 -msgid "Password confirmation" +#: inc/checkout/class-checkout.php:1837 +#: inc/helpers/validation-rules/class-unique.php:84 +msgid "A customer with the same email address or username already exists." msgstr "" #: inc/checkout/class-checkout.php:2191 -msgid "Email confirmation" +msgid "Password confirmation" msgstr "" #: inc/checkout/class-checkout.php:2192 -msgid "Template ID" +msgid "Email confirmation" msgstr "" #: inc/checkout/class-checkout.php:2193 +msgid "Template ID" +msgstr "" + +#: inc/checkout/class-checkout.php:2194 msgid "Valid password" msgstr "" -#: inc/checkout/class-checkout.php:2195 +#: inc/checkout/class-checkout.php:2196 msgid "Payment Gateway" msgstr "" #. translators: %s payment id. -#: inc/checkout/class-checkout.php:2387 +#: inc/checkout/class-checkout.php:2388 #, php-format msgid "Payment (%s) not found." msgstr "" #. translators: %s is the membership ID -#: inc/checkout/class-checkout.php:2522 +#: inc/checkout/class-checkout.php:2523 #, php-format msgid "Checkout failed for customer %s: " msgstr "" @@ -7322,7 +7373,7 @@ msgid "OFF" msgstr "" #: inc/checkout/class-legacy-checkout.php:252 -#: inc/models/class-membership.php:941 +#: inc/models/class-membership.php:958 #: inc/models/class-product.php:747 #: inc/models/class-product.php:839 #: views/checkout/templates/pricing-table/legacy.php:163 @@ -7332,7 +7383,7 @@ msgid "Free!" msgstr "" #: inc/checkout/class-legacy-checkout.php:253 -#: inc/list-tables/class-product-list-table.php:182 +#: inc/list-tables/class-product-list-table.php:186 #: inc/traits/trait-wp-ultimo-plan-deprecated.php:151 msgid "No Setup Fee" msgstr "" @@ -7435,15 +7486,16 @@ msgstr "" msgid "Please, do not use the \"site_url\" as one of your custom fields' ids. We use it as a honeytrap field to prevent spam registration. Consider alternatives such as \"url\" or \"website\"." msgstr "" -#. translators: %1$s the duration, and %2$s the duration unit (day, week, month, etc) -#: inc/checkout/class-line-item.php:1103 -#: inc/models/class-checkout-form.php:1321 -#: inc/models/class-checkout-form.php:1352 +#. translators: %1$s: the duration number, %2$s: the duration unit (days, weeks, months, etc). +#: inc/checkout/class-line-item.php:1109 +#: inc/list-tables/class-membership-list-table-widget.php:179 +#: inc/list-tables/class-membership-list-table.php:142 +#: inc/list-tables/class-product-list-table.php:149 +#: inc/models/class-membership.php:881 +#: inc/models/class-product.php:920 #, php-format -msgid "%2$s" -msgid_plural "every %1$s %2$s" -msgstr[0] "" -msgstr[1] "" +msgid "every %1$s %2$s" +msgstr "" #: inc/checkout/signup-fields/class-base-signup-field.php:311 msgid "This is a site-related field. For that reason, this field will not show up when no plans are present on the shopping cart." @@ -7667,13 +7719,13 @@ msgid "Existing customer?" msgstr "" #. translators: %s is the login URL. -#: inc/checkout/signup-fields/class-signup-field-email.php:294 +#: inc/checkout/signup-fields/class-signup-field-email.php:293 #, php-format msgid "Log in to renew or change an existing membership." msgstr "" #. translators: 1$s is the display name of the user currently logged in. -#: inc/checkout/signup-fields/class-signup-field-email.php:322 +#: inc/checkout/signup-fields/class-signup-field-email.php:321 #, php-format msgid "Not %1$s? Log in using your account." msgstr "" @@ -9817,7 +9869,7 @@ msgid "Quarterly" msgstr "" #: inc/compat/class-legacy-shortcodes.php:372 -#: inc/list-tables/class-product-list-table.php:313 +#: inc/list-tables/class-product-list-table.php:317 #: inc/models/class-checkout-form.php:551 #: inc/models/class-checkout-form.php:1380 msgid "Plans" @@ -13591,23 +13643,23 @@ msgid "Ready (without SSL)" msgstr "" #: inc/database/memberships/class-membership-status.php:70 -#: inc/list-tables/class-membership-list-table.php:281 +#: inc/list-tables/class-membership-list-table.php:285 msgid "Trialing" msgstr "" #: inc/database/memberships/class-membership-status.php:71 -#: inc/list-tables/class-membership-list-table.php:293 +#: inc/list-tables/class-membership-list-table.php:297 msgid "On Hold" msgstr "" #: inc/database/memberships/class-membership-status.php:72 -#: inc/list-tables/class-membership-list-table.php:299 +#: inc/list-tables/class-membership-list-table.php:303 msgid "Expired" msgstr "" #: inc/database/memberships/class-membership-status.php:73 #: inc/database/payments/class-payment-status.php:100 -#: inc/list-tables/class-membership-list-table.php:305 +#: inc/list-tables/class-membership-list-table.php:309 msgid "Cancelled" msgstr "" @@ -15333,155 +15385,156 @@ msgstr "" msgid "Unable to start the Stripe Connect authorization. Please try again or use direct API keys instead." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:555 +#: inc/gateways/class-base-stripe-gateway.php:564 msgid "Could not reach the Stripe Connect service to complete authorization. Please check your server's outbound connectivity and try again." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:564 +#: inc/gateways/class-base-stripe-gateway.php:573 msgid "Stripe Connect authorization was not accepted. The link may have expired — please try connecting again." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:572 +#: inc/gateways/class-base-stripe-gateway.php:581 msgid "Received an unexpected response while completing Stripe Connect. Please try again or use direct API keys instead." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:731 +#: inc/gateways/class-base-stripe-gateway.php:737 msgid "Change Payment Method" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:844 +#: inc/gateways/class-base-stripe-gateway.php:850 msgid "Manage your membership payment methods." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:877 +#: inc/gateways/class-base-stripe-gateway.php:883 #: inc/gateways/class-stripe-checkout-gateway.php:67 #: inc/gateways/class-stripe-gateway.php:97 msgid "Credit Card" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:894 +#: inc/gateways/class-base-stripe-gateway.php:900 msgid "Use Stripe Billing Portal" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:1040 -#: inc/gateways/class-base-stripe-gateway.php:1064 +#: inc/gateways/class-base-stripe-gateway.php:1046 +#: inc/gateways/class-base-stripe-gateway.php:1070 msgid "Invalid API Key provided" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:1168 +#. translators: %1$s: HTTP error code, %2$s: error message. +#: inc/gateways/class-base-stripe-gateway.php:1175 #, php-format msgid "Failed to add stripe webhook: %1$s, %2$s" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:1188 +#: inc/gateways/class-base-stripe-gateway.php:1195 #: inc/gateways/class-paypal-gateway.php:345 msgid "Error: No gateway subscription ID found for this membership." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:1272 +#: inc/gateways/class-base-stripe-gateway.php:1279 msgid "Amount adjustment based on custom deal." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:1556 +#: inc/gateways/class-base-stripe-gateway.php:1563 msgid "Invalid payment method" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:1762 +#: inc/gateways/class-base-stripe-gateway.php:1769 #: inc/gateways/class-paypal-gateway.php:585 #: inc/gateways/class-paypal-gateway.php:590 msgid "Account credit and other discounts" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2263 +#: inc/gateways/class-base-stripe-gateway.php:2270 #: inc/gateways/class-paypal-gateway.php:721 msgid "Gateway payment ID not found. Cannot process refund automatically." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2282 +#: inc/gateways/class-base-stripe-gateway.php:2289 msgid "Gateway payment ID not valid." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2426 +#: inc/gateways/class-base-stripe-gateway.php:2433 msgid "An unknown error has occurred." msgstr "" #. translators: 1 is the error code and 2 the message. -#: inc/gateways/class-base-stripe-gateway.php:2449 +#: inc/gateways/class-base-stripe-gateway.php:2456 #, php-format msgid "An error has occurred (code: %1$s; message: %2$s)." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2491 +#: inc/gateways/class-base-stripe-gateway.php:2498 msgid "Event ID not found." msgstr "" #. translators: %s is the customer ID. -#: inc/gateways/class-base-stripe-gateway.php:2594 +#: inc/gateways/class-base-stripe-gateway.php:2601 #, php-format msgid "Exiting Stripe webhook - This call must be handled by %s webhook" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2881 +#: inc/gateways/class-base-stripe-gateway.php:2888 msgid "Duplicate payment." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2894 +#: inc/gateways/class-base-stripe-gateway.php:2901 msgid "Payment not found on refund webhook call." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2900 +#: inc/gateways/class-base-stripe-gateway.php:2907 msgid "Payment is not refundable." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:2951 +#: inc/gateways/class-base-stripe-gateway.php:2958 msgid "Membership cancelled via Stripe webhook." msgstr "" #. translators: 1 is the card brand (e.g. VISA), and 2 is the last 4 digits. -#: inc/gateways/class-base-stripe-gateway.php:2980 +#: inc/gateways/class-base-stripe-gateway.php:2987 #, php-format msgid "%1$s ending in %2$s" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:3010 +#: inc/gateways/class-base-stripe-gateway.php:3017 msgid "Name on Card" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:3157 +#: inc/gateways/class-base-stripe-gateway.php:3164 msgid "Missing plan name or price." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:3178 +#: inc/gateways/class-base-stripe-gateway.php:3185 msgid "Empty plan ID." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:3259 +#: inc/gateways/class-base-stripe-gateway.php:3266 msgid "Missing product name." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:3271 +#: inc/gateways/class-base-stripe-gateway.php:3278 msgid "Empty product ID." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:3554 +#: inc/gateways/class-base-stripe-gateway.php:3561 msgid "Payment already completed." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:3563 +#: inc/gateways/class-base-stripe-gateway.php:3570 msgid "Payment is not in pending status." msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:3574 +#: inc/gateways/class-base-stripe-gateway.php:3581 msgid "No Stripe payment intent found for this payment." msgstr "" #. translators: %s is the intent status from Stripe. -#: inc/gateways/class-base-stripe-gateway.php:3597 +#: inc/gateways/class-base-stripe-gateway.php:3604 #, php-format msgid "Payment intent status is: %s" msgstr "" -#: inc/gateways/class-base-stripe-gateway.php:3625 +#: inc/gateways/class-base-stripe-gateway.php:3632 msgid "Payment verified and completed successfully." msgstr "" @@ -15957,16 +16010,16 @@ msgid "No application fee — thank you for your support!" msgstr "" #. translators: %s is the API URL. -#: inc/helpers/class-screenshot.php:61 +#: inc/helpers/class-screenshot.php:64 #, php-format msgid "Downloading image from \"%s\":" msgstr "" -#: inc/helpers/class-screenshot.php:86 +#: inc/helpers/class-screenshot.php:89 msgid "Result is not a PNG file." msgstr "" -#: inc/helpers/class-screenshot.php:125 +#: inc/helpers/class-screenshot.php:128 #: inc/installers/class-core-installer.php:71 #: inc/installers/class-core-installer.php:81 #: inc/installers/class-default-content-installer.php:155 @@ -16154,10 +16207,6 @@ msgstr "" msgid "The selected template is not available for this product." msgstr "" -#: inc/helpers/validation-rules/class-unique.php:84 -msgid "A customer with the same email address or username already exists." -msgstr "" - #. translators: %1$s opening a tag, %2$s closing a tag. #: inc/installers/class-core-installer.php:42 #, php-format @@ -16571,9 +16620,7 @@ msgstr "" msgid "Cannot connect to your cPanel server : Invalid Credentials" msgstr "" -#. translators: %s is the hosting provider name (e.g. Closte) #. translators: %s: hosting provider name. -#. translators: %s is the hosting provider name #: inc/integrations/providers/closte/class-closte-domain-mapping.php:59 #: inc/integrations/providers/cloudways/class-cloudways-domain-mapping.php:58 #: inc/integrations/providers/gridpane/class-gridpane-domain-mapping.php:57 @@ -16587,9 +16634,7 @@ msgstr "" msgid "Send API calls to %s servers with domain names added to this network" msgstr "" -#. translators: %s is the hosting provider name (e.g. Closte) #. translators: %s: hosting provider name. -#. translators: %s is the hosting provider name #: inc/integrations/providers/closte/class-closte-domain-mapping.php:61 #: inc/integrations/providers/cloudways/class-cloudways-domain-mapping.php:60 #: inc/integrations/providers/gridpane/class-gridpane-domain-mapping.php:59 @@ -17050,7 +17095,7 @@ msgid "Your Rocket.net account password." msgstr "" #: inc/integrations/providers/rocket/class-rocket-integration.php:93 -#: views/checkout/partials/inline-login-prompt.php:38 +#: views/checkout/partials/inline-login-prompt.php:30 msgid "Enter your password" msgstr "" @@ -17402,9 +17447,9 @@ msgstr "" #: inc/list-tables/class-domain-list-table.php:167 #: inc/list-tables/class-email-list-table.php:201 #: inc/list-tables/class-event-list-table.php:215 -#: inc/list-tables/class-membership-list-table.php:183 +#: inc/list-tables/class-membership-list-table.php:187 #: inc/list-tables/class-payment-list-table.php:201 -#: inc/list-tables/class-product-list-table.php:258 +#: inc/list-tables/class-product-list-table.php:262 #: inc/list-tables/class-site-list-table.php:276 #: inc/list-tables/class-webhook-list-table.php:179 #: views/emails/admin/domain-created.php:24 @@ -17522,7 +17567,7 @@ msgstr "" #: inc/list-tables/class-customer-list-table.php:241 #: inc/list-tables/class-discount-code-list-table.php:189 -#: inc/list-tables/class-product-list-table.php:253 +#: inc/list-tables/class-product-list-table.php:257 #: inc/list-tables/class-webhook-list-table.php:173 #: views/base/checkout-forms/js-templates.php:58 #: views/email/widget-placeholders.php:29 @@ -17551,38 +17596,42 @@ msgstr "" msgid "Online" msgstr "" -#. translators: %s is the product name, %2$s is the count of other products. -#: inc/list-tables/class-customers-membership-list-table.php:54 +#. translators: %s: the product name. +#: inc/list-tables/class-customers-membership-list-table.php:59 #, php-format -msgid "Contains %1$s" -msgid_plural "Contains %1$s and %2$s other product(s)" -msgstr[0] "" -msgstr[1] "" +msgid "Contains %s" +msgstr "" -#: inc/list-tables/class-customers-membership-list-table.php:71 +#. translators: %1$s: the product name, %2$s: the count of other products. +#: inc/list-tables/class-customers-membership-list-table.php:62 +#, php-format +msgid "Contains %1$s and %2$s other product(s)" +msgstr "" + +#: inc/list-tables/class-customers-membership-list-table.php:80 #: inc/list-tables/class-customers-payment-list-table.php:62 msgid "Payment Total" msgstr "" -#: inc/list-tables/class-customers-membership-list-table.php:88 +#: inc/list-tables/class-customers-membership-list-table.php:97 #: views/dashboard-widgets/current-membership.php:169 msgid "Expires" msgstr "" #. translators: %s is a placeholder for the human-readable time difference, e.g., "2 hours ago" -#: inc/list-tables/class-customers-membership-list-table.php:90 +#: inc/list-tables/class-customers-membership-list-table.php:99 #, php-format msgid "Expired %s" msgstr "" #. translators: %s is a placeholder for the human-readable time difference, e.g., "2 hours ago" -#: inc/list-tables/class-customers-membership-list-table.php:90 +#: inc/list-tables/class-customers-membership-list-table.php:99 #, php-format msgid "Expiring %s" msgstr "" #. translators: %s is a placeholder for the human-readable time difference, e.g., "2 hours ago" -#: inc/list-tables/class-customers-membership-list-table.php:96 +#: inc/list-tables/class-customers-membership-list-table.php:105 #: inc/list-tables/class-customers-payment-list-table.php:76 #: inc/list-tables/class-customers-site-list-table.php:108 #: inc/list-tables/class-memberships-site-list-table.php:83 @@ -17785,7 +17834,7 @@ msgid "Fatal" msgstr "" #: inc/list-tables/class-event-list-table.php:243 -#: inc/list-tables/class-membership-list-table.php:234 +#: inc/list-tables/class-membership-list-table.php:238 #: inc/list-tables/class-payment-list-table.php:250 msgid "Created At" msgstr "" @@ -17875,42 +17924,30 @@ msgstr "" msgid "Upgrade or Downgrade" msgstr "" -#. translators: %1$s the duration, and %2$s the duration unit (day, week, month, etc) -#: inc/list-tables/class-membership-list-table-widget.php:176 -#: inc/list-tables/class-membership-list-table.php:139 -#: inc/list-tables/class-product-list-table.php:146 -#: inc/models/class-membership.php:874 -#: inc/models/class-product.php:901 -#, php-format -msgid "every %2$s" -msgid_plural "every %1$s %2$s" -msgstr[0] "" -msgstr[1] "" - #. translators: %s is the number of billing cycles. -#: inc/list-tables/class-membership-list-table-widget.php:184 -#: inc/list-tables/class-membership-list-table.php:147 -#: inc/list-tables/class-product-list-table.php:154 -#: inc/models/class-membership.php:926 -#: inc/models/class-product.php:858 +#: inc/list-tables/class-membership-list-table-widget.php:188 +#: inc/list-tables/class-membership-list-table.php:151 +#: inc/list-tables/class-product-list-table.php:158 +#: inc/models/class-membership.php:943 +#: inc/models/class-product.php:870 #, php-format msgid "for %s cycle" msgid_plural "for %s cycles" msgstr[0] "" msgstr[1] "" -#: inc/list-tables/class-membership-list-table-widget.php:191 -#: inc/list-tables/class-membership-list-table.php:156 -#: inc/list-tables/class-product-list-table.php:161 +#: inc/list-tables/class-membership-list-table-widget.php:195 +#: inc/list-tables/class-membership-list-table.php:160 +#: inc/list-tables/class-product-list-table.php:165 msgid "one time payment" msgstr "" -#: inc/list-tables/class-membership-list-table-widget.php:262 +#: inc/list-tables/class-membership-list-table-widget.php:266 #: inc/list-tables/class-payment-list-table-widget.php:219 msgid "Ref." msgstr "" -#: inc/list-tables/class-membership-list-table.php:171 +#: inc/list-tables/class-membership-list-table.php:175 #: inc/list-tables/class-payment-list-table.php:190 #: views/emails/admin/domain-created.php:122 #: views/emails/admin/payment-received.php:54 @@ -17919,22 +17956,22 @@ msgstr "" msgid "Reference Code" msgstr "" -#: inc/list-tables/class-membership-list-table.php:182 +#: inc/list-tables/class-membership-list-table.php:186 #: views/emails/admin/domain-created.php:128 #: views/emails/admin/payment-received.php:113 #: views/emails/admin/site-published.php:79 msgid "Expiration" msgstr "" -#: inc/list-tables/class-membership-list-table.php:202 +#: inc/list-tables/class-membership-list-table.php:206 msgid "It never expires" msgstr "" -#: inc/list-tables/class-membership-list-table.php:250 +#: inc/list-tables/class-membership-list-table.php:254 msgid "Renewal Date" msgstr "" -#: inc/list-tables/class-membership-list-table.php:269 +#: inc/list-tables/class-membership-list-table.php:273 msgid "All Memberships" msgstr "" @@ -17987,19 +18024,19 @@ msgid "All Payments" msgstr "" #: inc/list-tables/class-product-list-table.php:132 -#: inc/list-tables/class-product-list-table.php:178 +#: inc/list-tables/class-product-list-table.php:182 msgid "Requires contact" msgstr "" -#: inc/list-tables/class-product-list-table.php:307 +#: inc/list-tables/class-product-list-table.php:311 msgid "All Products" msgstr "" -#: inc/list-tables/class-product-list-table.php:319 +#: inc/list-tables/class-product-list-table.php:323 msgid "Packages" msgstr "" -#: inc/list-tables/class-product-list-table.php:325 +#: inc/list-tables/class-product-list-table.php:329 msgid "Services" msgstr "" @@ -18613,19 +18650,19 @@ msgstr "" msgid "Use the Manual Gateway to allow users to pay you directly via bank transfers, checks, or other channels." msgstr "" -#: inc/managers/class-gateway-manager.php:601 +#: inc/managers/class-gateway-manager.php:603 msgid "Payment hash is required." msgstr "" -#: inc/managers/class-gateway-manager.php:615 +#: inc/managers/class-gateway-manager.php:617 msgid "Payment completed." msgstr "" -#: inc/managers/class-gateway-manager.php:633 +#: inc/managers/class-gateway-manager.php:635 msgid "Non-Stripe payment, cannot verify." msgstr "" -#: inc/managers/class-gateway-manager.php:645 +#: inc/managers/class-gateway-manager.php:647 msgid "Gateway does not support verification." msgstr "" @@ -19192,29 +19229,34 @@ msgid "Schedule date is invalid." msgstr "" #. translators: times billed / subscription duration in cycles. e.g. 1/12 cycles -#: inc/models/class-membership.php:890 +#: inc/models/class-membership.php:895 #, php-format msgid "%1$s / %2$s cycles" msgstr "" #. translators: the place holder is the number of times the membership was billed. -#: inc/models/class-membership.php:895 +#: inc/models/class-membership.php:900 #, php-format msgid "%1$s / until cancelled" msgstr "" -#. translators: %1$s is the formatted price, %2$s the duration, and %3$s the duration unit (day, week, month, etc) -#: inc/models/class-membership.php:915 -#: inc/models/class-product.php:847 +#. translators: %1$s: the formatted price, %2$s: the duration unit (day, week, month, etc). +#: inc/models/class-membership.php:924 +#: inc/models/class-product.php:851 #, php-format -msgid "%1$s every %3$s" -msgid_plural "%1$s every %2$s %3$s" -msgstr[0] "" -msgstr[1] "" +msgid "%1$s / %2$s" +msgstr "" + +#. translators: %1$s: the formatted price, %2$s: the duration number, %3$s: the duration unit (days, weeks, months, etc). +#: inc/models/class-membership.php:931 +#: inc/models/class-product.php:858 +#, php-format +msgid "%1$s every %2$s %3$s" +msgstr "" #. translators: %1$s is the formatted price of the product -#: inc/models/class-membership.php:935 -#: inc/models/class-product.php:867 +#: inc/models/class-membership.php:952 +#: inc/models/class-product.php:879 #, php-format msgid "%1$s one time payment" msgstr "" @@ -19252,12 +19294,12 @@ msgid "Contact us" msgstr "" #. translators: %1$s is the formatted price of the setup fee -#: inc/models/class-product.php:875 +#: inc/models/class-product.php:887 #, php-format msgid "Setup Fee of %1$s" msgstr "" -#: inc/models/class-product.php:896 +#: inc/models/class-product.php:908 msgid "one-time payment" msgstr "" @@ -20673,7 +20715,6 @@ msgstr "" #: inc/ui/class-tours.php:102 #: views/base/filter.php:65 -#: views/checkout/partials/inline-login-prompt.php:23 msgid "Close" msgstr "" diff --git a/readme.txt b/readme.txt index 79102064..c41b5561 100644 --- a/readme.txt +++ b/readme.txt @@ -234,15 +234,22 @@ Version [2.4.11] - Released on 2026-XX-XX - New: Stripe Connect via secure proxy server — platform credentials no longer distributed in plugin code. - New: Stripe Checkout Element with automatic billing address handling and removal of application fees. - New: Multisite Setup Wizard — guides single-site installs through enabling and configuring WordPress Multisite. -- New: Form field normalization CSS with CSS custom properties for consistent checkout and login styling across all themes and page builders. -- Fix: Problems with choosing country and state in checkout. +- New: Modular hosting integration system with encrypted credential storage. +- New: Form field normalization CSS for consistent checkout and login styling across all themes and page builders. +- Fix: Password strength setting not being applied during checkout. +- Fix: Encoded characters stripped from URLs during SSO and domain mapping redirects. +- Fix: Inline login prompt stability and missing validation for existing emails at checkout. +- Fix: Site title field error caused by third-party plugin conflicts. +- Fix: URL replacement failing for Elementor content on subdirectory multisite installs. +- Fix: Country and state selection issues in checkout. - Fix: Duplicate Country/ZIP fields appearing on Stripe checkout. - Fix: Invoice PDF download failing with expired nonce. - Fix: Settings page crash on PHP 8.4. - Fix: Single-site compatibility issues and dashboard widget setup status detection. - Fix: Rewrite rules now flushed when signup pages are created or modified. -- Improved: 40+ new unit tests covering signup fields, admin pages, database enums, and more. -- Improved: Stripe checkout and subscription renewal E2E tests. +- Improved: Admin pages no longer loaded on frontend and cron requests for better performance. +- Improved: Security hardening for input validation, credential storage, and cart processing. +- Improved: Expanded automated test coverage across checkout, payments, and admin functionality. Version [2.4.10] - Released on 2026-01-23 - New: Configurable minimum password strength setting with Medium, Strong, and Super Strong options. From 42367a372c95d3ffd87bb848e704c027c4985e4e Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 16 Feb 2026 19:14:55 -0700 Subject: [PATCH 10/10] Fix Product_Test assertions for duration=1 price descriptions The "every" keyword only appears when duration > 1 (e.g., "every 3 months"). With duration=1, the format is "$19.99 / month" and "month" respectively. Co-Authored-By: Claude Opus 4.6 --- tests/WP_Ultimo/Models/Product_Test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/WP_Ultimo/Models/Product_Test.php b/tests/WP_Ultimo/Models/Product_Test.php index 54335768..0eed7e67 100644 --- a/tests/WP_Ultimo/Models/Product_Test.php +++ b/tests/WP_Ultimo/Models/Product_Test.php @@ -904,7 +904,7 @@ public function test_get_price_description_recurring(): void { $desc = $this->product->get_price_description(); $this->assertIsString($desc); $this->assertNotEmpty($desc); - $this->assertStringContainsString('every', $desc); + $this->assertStringContainsString('/ month', $desc); } /** @@ -1086,7 +1086,7 @@ public function test_get_recurring_description_recurring(): void { $this->product->set_duration_unit('month'); $desc = $this->product->get_recurring_description(); - $this->assertStringContainsString('every', $desc); + $this->assertStringContainsString('month', $desc); } // ========================================================================