v){const t=v-M+e.length;return t>0&&A.push(e.slice(0,t)),A.truncated=!0,A.bytesRead=v,a.removeAllListeners("data"),void A.emit("limit")}A.push(e)||(s._pause=!0),A.bytesRead=M},N=function(){T=void 0,A.push(null)}}else{if(L===R)return e.hitFieldsLimit||(e.hitFieldsLimit=!0,e.emit("fieldsLimit")),m(a);++L,++_;let A="",t=!1;F=a,S=function(e){if((M+=e.length)>b){const r=b-(M-e.length);A+=e.toString("binary",0,r),t=!0,a.removeAllListeners("data")}else A+=e.toString("binary")},N=function(){F=void 0,A.length&&(A=i(A,"binary",C)),e.emit("field",d,A,!1,t,B,p),--_,y()}}a._readableState.sync=!1,a.on("data",S),a.on("end",N)})).on("error",(function(e){T&&T.emit("error",e)}))})).on("error",(function(A){e.emit("error",A)})).on("finish",(function(){M=!0,y()}))}function m(e){e.resume()}function h(e){r.call(this,e),this.bytesRead=0,this.truncated=!1}E.detect=/^multipart\/form-data/i,E.prototype.write=function(e,A){const t=this.parser.write(e);t&&!this._pause?A():(this._needDrain=!t,this._cb=A)},E.prototype.end=function(){const e=this;e.parser.writable?e.parser.end():e._boy._done||process.nextTick((function(){e._boy._done=!0,e._boy.emit("finish")}))},s(h,r),h.prototype._read=function(e){},e.exports=E},4314:(e,A,t)=>{"use strict";const r=t(4002),s=t(7246),a=t(7636),o=/^charset$/i;function i(e,A){const t=A.limits,s=A.parsedConType;let i;this.boy=e,this.fieldSizeLimit=a(t,"fieldSize",1048576),this.fieldNameSizeLimit=a(t,"fieldNameSize",100),this.fieldsLimit=a(t,"fields",1/0);for(var n=0,p=s.length;no&&(this._key+=this.decoder.write(e.toString("binary",o,t))),this._state="val",this._hitLimit=!1,this._checkingBytes=!0,this._val="",this._bytesVal=0,this._valTrunc=!1,this.decoder.reset(),o=t+1;else if(void 0!==r){let t;++this._fields;const a=this._keyTrunc;if(t=r>o?this._key+=this.decoder.write(e.toString("binary",o,r)):this._key,this._hitLimit=!1,this._checkingBytes=!0,this._key="",this._bytesKey=0,this._keyTrunc=!1,this.decoder.reset(),t.length&&this.boy.emit("field",s(t,"binary",this.charset),"",a,!1),o=r+1,this._fields===this.fieldsLimit)return A()}else this._hitLimit?(a>o&&(this._key+=this.decoder.write(e.toString("binary",o,a))),o=a,(this._bytesKey=this._key.length)===this.fieldNameSizeLimit&&(this._checkingBytes=!1,this._keyTrunc=!0)):(oo&&(this._val+=this.decoder.write(e.toString("binary",o,r))),this.boy.emit("field",s(this._key,"binary",this.charset),s(this._val,"binary",this.charset),this._keyTrunc,this._valTrunc),this._state="key",this._hitLimit=!1,this._checkingBytes=!0,this._key="",this._bytesKey=0,this._keyTrunc=!1,this.decoder.reset(),o=r+1,this._fields===this.fieldsLimit)return A()}else this._hitLimit?(a>o&&(this._val+=this.decoder.write(e.toString("binary",o,a))),o=a,(""===this._val&&0===this.fieldSizeLimit||(this._bytesVal=this._val.length)===this.fieldSizeLimit)&&(this._checkingBytes=!1,this._valTrunc=!0)):(o0?this.boy.emit("field",s(this._key,"binary",this.charset),"",this._keyTrunc,!1):"val"===this._state&&this.boy.emit("field",s(this._key,"binary",this.charset),s(this._val,"binary",this.charset),this._keyTrunc,this._valTrunc),this.boy._done=!0,this.boy.emit("finish"))},e.exports=i},4002:e=>{"use strict";const A=/\+/g,t=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];function r(){this.buffer=void 0}r.prototype.write=function(e){let r="",s=0,a=0;const o=(e=e.replace(A," ")).length;for(;sa&&(r+=e.substring(a,s),a=s),this.buffer="",++a);return a{"use strict";e.exports=function(e){if("string"!=typeof e)return"";for(var A=e.length-1;A>=0;--A)switch(e.charCodeAt(A)){case 47:case 92:return".."===(e=e.slice(A+1))||"."===e?"":e}return".."===e||"."===e?"":e}},7246:function(e){"use strict";const A=new TextDecoder("utf-8"),t=new Map([["utf-8",A],["utf8",A]]),r={utf8:(e,A)=>0===e.length?"":("string"==typeof e&&(e=Buffer.from(e,A)),e.utf8Slice(0,e.length)),latin1:(e,A)=>0===e.length?"":"string"==typeof e?e:e.latin1Slice(0,e.length),utf16le:(e,A)=>0===e.length?"":("string"==typeof e&&(e=Buffer.from(e,A)),e.ucs2Slice(0,e.length)),base64:(e,A)=>0===e.length?"":("string"==typeof e&&(e=Buffer.from(e,A)),e.base64Slice(0,e.length)),other:(e,A)=>{if(0===e.length)return"";if("string"==typeof e&&(e=Buffer.from(e,A)),t.has(this.toString()))try{return t.get(this).decode(e)}catch{}return"string"==typeof e?e:e.toString()}};e.exports=function(e,A,t){return e?function(e){let A;for(;;)switch(e){case"utf-8":case"utf8":return r.utf8;case"latin1":case"ascii":case"us-ascii":case"iso-8859-1":case"iso8859-1":case"iso88591":case"iso_8859-1":case"windows-1252":case"iso_8859-1:1987":case"cp1252":case"x-cp1252":return r.latin1;case"utf16le":case"utf-16le":case"ucs2":case"ucs-2":return r.utf16le;case"base64":return r.base64;default:if(void 0===A){A=!0,e=e.toLowerCase();continue}return r.other.bind(e)}}(t)(e,A):e}},7636:e=>{"use strict";e.exports=function(e,A,t){if(!e||void 0===e[A]||null===e[A])return t;if("number"!=typeof e[A]||isNaN(e[A]))throw new TypeError("Limit "+A+" is not a valid number");return e[A]}},9825:(e,A,t)=>{"use strict";const r=t(7246),s=/%[a-fA-F0-9][a-fA-F0-9]/g,a={"%00":"\0","%01":"","%02":"","%03":"","%04":"","%05":"","%06":"","%07":"","%08":"\b","%09":"\t","%0a":"\n","%0A":"\n","%0b":"\v","%0B":"\v","%0c":"\f","%0C":"\f","%0d":"\r","%0D":"\r","%0e":"","%0E":"","%0f":"","%0F":"","%10":"","%11":"","%12":"","%13":"","%14":"","%15":"","%16":"","%17":"","%18":"","%19":"","%1a":"","%1A":"","%1b":"","%1B":"","%1c":"","%1C":"","%1d":"","%1D":"","%1e":"","%1E":"","%1f":"","%1F":"","%20":" ","%21":"!","%22":'"',"%23":"#","%24":"$","%25":"%","%26":"&","%27":"'","%28":"(","%29":")","%2a":"*","%2A":"*","%2b":"+","%2B":"+","%2c":",","%2C":",","%2d":"-","%2D":"-","%2e":".","%2E":".","%2f":"/","%2F":"/","%30":"0","%31":"1","%32":"2","%33":"3","%34":"4","%35":"5","%36":"6","%37":"7","%38":"8","%39":"9","%3a":":","%3A":":","%3b":";","%3B":";","%3c":"<","%3C":"<","%3d":"=","%3D":"=","%3e":">","%3E":">","%3f":"?","%3F":"?","%40":"@","%41":"A","%42":"B","%43":"C","%44":"D","%45":"E","%46":"F","%47":"G","%48":"H","%49":"I","%4a":"J","%4A":"J","%4b":"K","%4B":"K","%4c":"L","%4C":"L","%4d":"M","%4D":"M","%4e":"N","%4E":"N","%4f":"O","%4F":"O","%50":"P","%51":"Q","%52":"R","%53":"S","%54":"T","%55":"U","%56":"V","%57":"W","%58":"X","%59":"Y","%5a":"Z","%5A":"Z","%5b":"[","%5B":"[","%5c":"\\","%5C":"\\","%5d":"]","%5D":"]","%5e":"^","%5E":"^","%5f":"_","%5F":"_","%60":"`","%61":"a","%62":"b","%63":"c","%64":"d","%65":"e","%66":"f","%67":"g","%68":"h","%69":"i","%6a":"j","%6A":"j","%6b":"k","%6B":"k","%6c":"l","%6C":"l","%6d":"m","%6D":"m","%6e":"n","%6E":"n","%6f":"o","%6F":"o","%70":"p","%71":"q","%72":"r","%73":"s","%74":"t","%75":"u","%76":"v","%77":"w","%78":"x","%79":"y","%7a":"z","%7A":"z","%7b":"{","%7B":"{","%7c":"|","%7C":"|","%7d":"}","%7D":"}","%7e":"~","%7E":"~","%7f":"","%7F":"","%80":"","%81":"","%82":"","%83":"","%84":"","%85":"
","%86":"","%87":"","%88":"","%89":"","%8a":"","%8A":"","%8b":"","%8B":"","%8c":"","%8C":"","%8d":"","%8D":"","%8e":"","%8E":"","%8f":"","%8F":"","%90":"","%91":"","%92":"","%93":"","%94":"","%95":"","%96":"","%97":"","%98":"","%99":"","%9a":"","%9A":"","%9b":"","%9B":"","%9c":"","%9C":"","%9d":"","%9D":"","%9e":"","%9E":"","%9f":"","%9F":"","%a0":" ","%A0":" ","%a1":"¡","%A1":"¡","%a2":"¢","%A2":"¢","%a3":"£","%A3":"£","%a4":"¤","%A4":"¤","%a5":"¥","%A5":"¥","%a6":"¦","%A6":"¦","%a7":"§","%A7":"§","%a8":"¨","%A8":"¨","%a9":"©","%A9":"©","%aa":"ª","%Aa":"ª","%aA":"ª","%AA":"ª","%ab":"«","%Ab":"«","%aB":"«","%AB":"«","%ac":"¬","%Ac":"¬","%aC":"¬","%AC":"¬","%ad":"","%Ad":"","%aD":"","%AD":"","%ae":"®","%Ae":"®","%aE":"®","%AE":"®","%af":"¯","%Af":"¯","%aF":"¯","%AF":"¯","%b0":"°","%B0":"°","%b1":"±","%B1":"±","%b2":"²","%B2":"²","%b3":"³","%B3":"³","%b4":"´","%B4":"´","%b5":"µ","%B5":"µ","%b6":"¶","%B6":"¶","%b7":"·","%B7":"·","%b8":"¸","%B8":"¸","%b9":"¹","%B9":"¹","%ba":"º","%Ba":"º","%bA":"º","%BA":"º","%bb":"»","%Bb":"»","%bB":"»","%BB":"»","%bc":"¼","%Bc":"¼","%bC":"¼","%BC":"¼","%bd":"½","%Bd":"½","%bD":"½","%BD":"½","%be":"¾","%Be":"¾","%bE":"¾","%BE":"¾","%bf":"¿","%Bf":"¿","%bF":"¿","%BF":"¿","%c0":"À","%C0":"À","%c1":"Á","%C1":"Á","%c2":"Â","%C2":"Â","%c3":"Ã","%C3":"Ã","%c4":"Ä","%C4":"Ä","%c5":"Å","%C5":"Å","%c6":"Æ","%C6":"Æ","%c7":"Ç","%C7":"Ç","%c8":"È","%C8":"È","%c9":"É","%C9":"É","%ca":"Ê","%Ca":"Ê","%cA":"Ê","%CA":"Ê","%cb":"Ë","%Cb":"Ë","%cB":"Ë","%CB":"Ë","%cc":"Ì","%Cc":"Ì","%cC":"Ì","%CC":"Ì","%cd":"Í","%Cd":"Í","%cD":"Í","%CD":"Í","%ce":"Î","%Ce":"Î","%cE":"Î","%CE":"Î","%cf":"Ï","%Cf":"Ï","%cF":"Ï","%CF":"Ï","%d0":"Ð","%D0":"Ð","%d1":"Ñ","%D1":"Ñ","%d2":"Ò","%D2":"Ò","%d3":"Ó","%D3":"Ó","%d4":"Ô","%D4":"Ô","%d5":"Õ","%D5":"Õ","%d6":"Ö","%D6":"Ö","%d7":"×","%D7":"×","%d8":"Ø","%D8":"Ø","%d9":"Ù","%D9":"Ù","%da":"Ú","%Da":"Ú","%dA":"Ú","%DA":"Ú","%db":"Û","%Db":"Û","%dB":"Û","%DB":"Û","%dc":"Ü","%Dc":"Ü","%dC":"Ü","%DC":"Ü","%dd":"Ý","%Dd":"Ý","%dD":"Ý","%DD":"Ý","%de":"Þ","%De":"Þ","%dE":"Þ","%DE":"Þ","%df":"ß","%Df":"ß","%dF":"ß","%DF":"ß","%e0":"à","%E0":"à","%e1":"á","%E1":"á","%e2":"â","%E2":"â","%e3":"ã","%E3":"ã","%e4":"ä","%E4":"ä","%e5":"å","%E5":"å","%e6":"æ","%E6":"æ","%e7":"ç","%E7":"ç","%e8":"è","%E8":"è","%e9":"é","%E9":"é","%ea":"ê","%Ea":"ê","%eA":"ê","%EA":"ê","%eb":"ë","%Eb":"ë","%eB":"ë","%EB":"ë","%ec":"ì","%Ec":"ì","%eC":"ì","%EC":"ì","%ed":"í","%Ed":"í","%eD":"í","%ED":"í","%ee":"î","%Ee":"î","%eE":"î","%EE":"î","%ef":"ï","%Ef":"ï","%eF":"ï","%EF":"ï","%f0":"ð","%F0":"ð","%f1":"ñ","%F1":"ñ","%f2":"ò","%F2":"ò","%f3":"ó","%F3":"ó","%f4":"ô","%F4":"ô","%f5":"õ","%F5":"õ","%f6":"ö","%F6":"ö","%f7":"÷","%F7":"÷","%f8":"ø","%F8":"ø","%f9":"ù","%F9":"ù","%fa":"ú","%Fa":"ú","%fA":"ú","%FA":"ú","%fb":"û","%Fb":"û","%fB":"û","%FB":"û","%fc":"ü","%Fc":"ü","%fC":"ü","%FC":"ü","%fd":"ý","%Fd":"ý","%fD":"ý","%FD":"ý","%fe":"þ","%Fe":"þ","%fE":"þ","%FE":"þ","%ff":"ÿ","%Ff":"ÿ","%fF":"ÿ","%FF":"ÿ"};function o(e){return a[e]}e.exports=function(e){const A=[];let t=0,a="",i=!1,n=!1,p=0,d="";const l=e.length;for(var c=0;c{"use strict";e.exports=JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"],[[47,47],"disallowed_STD3_valid"],[[48,57],"valid"],[[58,64],"disallowed_STD3_valid"],[[65,65],"mapped",[97]],[[66,66],"mapped",[98]],[[67,67],"mapped",[99]],[[68,68],"mapped",[100]],[[69,69],"mapped",[101]],[[70,70],"mapped",[102]],[[71,71],"mapped",[103]],[[72,72],"mapped",[104]],[[73,73],"mapped",[105]],[[74,74],"mapped",[106]],[[75,75],"mapped",[107]],[[76,76],"mapped",[108]],[[77,77],"mapped",[109]],[[78,78],"mapped",[110]],[[79,79],"mapped",[111]],[[80,80],"mapped",[112]],[[81,81],"mapped",[113]],[[82,82],"mapped",[114]],[[83,83],"mapped",[115]],[[84,84],"mapped",[116]],[[85,85],"mapped",[117]],[[86,86],"mapped",[118]],[[87,87],"mapped",[119]],[[88,88],"mapped",[120]],[[89,89],"mapped",[121]],[[90,90],"mapped",[122]],[[91,96],"disallowed_STD3_valid"],[[97,122],"valid"],[[123,127],"disallowed_STD3_valid"],[[128,159],"disallowed"],[[160,160],"disallowed_STD3_mapped",[32]],[[161,167],"valid",[],"NV8"],[[168,168],"disallowed_STD3_mapped",[32,776]],[[169,169],"valid",[],"NV8"],[[170,170],"mapped",[97]],[[171,172],"valid",[],"NV8"],[[173,173],"ignored"],[[174,174],"valid",[],"NV8"],[[175,175],"disallowed_STD3_mapped",[32,772]],[[176,177],"valid",[],"NV8"],[[178,178],"mapped",[50]],[[179,179],"mapped",[51]],[[180,180],"disallowed_STD3_mapped",[32,769]],[[181,181],"mapped",[956]],[[182,182],"valid",[],"NV8"],[[183,183],"valid"],[[184,184],"disallowed_STD3_mapped",[32,807]],[[185,185],"mapped",[49]],[[186,186],"mapped",[111]],[[187,187],"valid",[],"NV8"],[[188,188],"mapped",[49,8260,52]],[[189,189],"mapped",[49,8260,50]],[[190,190],"mapped",[51,8260,52]],[[191,191],"valid",[],"NV8"],[[192,192],"mapped",[224]],[[193,193],"mapped",[225]],[[194,194],"mapped",[226]],[[195,195],"mapped",[227]],[[196,196],"mapped",[228]],[[197,197],"mapped",[229]],[[198,198],"mapped",[230]],[[199,199],"mapped",[231]],[[200,200],"mapped",[232]],[[201,201],"mapped",[233]],[[202,202],"mapped",[234]],[[203,203],"mapped",[235]],[[204,204],"mapped",[236]],[[205,205],"mapped",[237]],[[206,206],"mapped",[238]],[[207,207],"mapped",[239]],[[208,208],"mapped",[240]],[[209,209],"mapped",[241]],[[210,210],"mapped",[242]],[[211,211],"mapped",[243]],[[212,212],"mapped",[244]],[[213,213],"mapped",[245]],[[214,214],"mapped",[246]],[[215,215],"valid",[],"NV8"],[[216,216],"mapped",[248]],[[217,217],"mapped",[249]],[[218,218],"mapped",[250]],[[219,219],"mapped",[251]],[[220,220],"mapped",[252]],[[221,221],"mapped",[253]],[[222,222],"mapped",[254]],[[223,223],"deviation",[115,115]],[[224,246],"valid"],[[247,247],"valid",[],"NV8"],[[248,255],"valid"],[[256,256],"mapped",[257]],[[257,257],"valid"],[[258,258],"mapped",[259]],[[259,259],"valid"],[[260,260],"mapped",[261]],[[261,261],"valid"],[[262,262],"mapped",[263]],[[263,263],"valid"],[[264,264],"mapped",[265]],[[265,265],"valid"],[[266,266],"mapped",[267]],[[267,267],"valid"],[[268,268],"mapped",[269]],[[269,269],"valid"],[[270,270],"mapped",[271]],[[271,271],"valid"],[[272,272],"mapped",[273]],[[273,273],"valid"],[[274,274],"mapped",[275]],[[275,275],"valid"],[[276,276],"mapped",[277]],[[277,277],"valid"],[[278,278],"mapped",[279]],[[279,279],"valid"],[[280,280],"mapped",[281]],[[281,281],"valid"],[[282,282],"mapped",[283]],[[283,283],"valid"],[[284,284],"mapped",[285]],[[285,285],"valid"],[[286,286],"mapped",[287]],[[287,287],"valid"],[[288,288],"mapped",[289]],[[289,289],"valid"],[[290,290],"mapped",[291]],[[291,291],"valid"],[[292,292],"mapped",[293]],[[293,293],"valid"],[[294,294],"mapped",[295]],[[295,295],"valid"],[[296,296],"mapped",[297]],[[297,297],"valid"],[[298,298],"mapped",[299]],[[299,299],"valid"],[[300,300],"mapped",[301]],[[301,301],"valid"],[[302,302],"mapped",[303]],[[303,303],"valid"],[[304,304],"mapped",[105,775]],[[305,305],"valid"],[[306,307],"mapped",[105,106]],[[308,308],"mapped",[309]],[[309,309],"valid"],[[310,310],"mapped",[311]],[[311,312],"valid"],[[313,313],"mapped",[314]],[[314,314],"valid"],[[315,315],"mapped",[316]],[[316,316],"valid"],[[317,317],"mapped",[318]],[[318,318],"valid"],[[319,320],"mapped",[108,183]],[[321,321],"mapped",[322]],[[322,322],"valid"],[[323,323],"mapped",[324]],[[324,324],"valid"],[[325,325],"mapped",[326]],[[326,326],"valid"],[[327,327],"mapped",[328]],[[328,328],"valid"],[[329,329],"mapped",[700,110]],[[330,330],"mapped",[331]],[[331,331],"valid"],[[332,332],"mapped",[333]],[[333,333],"valid"],[[334,334],"mapped",[335]],[[335,335],"valid"],[[336,336],"mapped",[337]],[[337,337],"valid"],[[338,338],"mapped",[339]],[[339,339],"valid"],[[340,340],"mapped",[341]],[[341,341],"valid"],[[342,342],"mapped",[343]],[[343,343],"valid"],[[344,344],"mapped",[345]],[[345,345],"valid"],[[346,346],"mapped",[347]],[[347,347],"valid"],[[348,348],"mapped",[349]],[[349,349],"valid"],[[350,350],"mapped",[351]],[[351,351],"valid"],[[352,352],"mapped",[353]],[[353,353],"valid"],[[354,354],"mapped",[355]],[[355,355],"valid"],[[356,356],"mapped",[357]],[[357,357],"valid"],[[358,358],"mapped",[359]],[[359,359],"valid"],[[360,360],"mapped",[361]],[[361,361],"valid"],[[362,362],"mapped",[363]],[[363,363],"valid"],[[364,364],"mapped",[365]],[[365,365],"valid"],[[366,366],"mapped",[367]],[[367,367],"valid"],[[368,368],"mapped",[369]],[[369,369],"valid"],[[370,370],"mapped",[371]],[[371,371],"valid"],[[372,372],"mapped",[373]],[[373,373],"valid"],[[374,374],"mapped",[375]],[[375,375],"valid"],[[376,376],"mapped",[255]],[[377,377],"mapped",[378]],[[378,378],"valid"],[[379,379],"mapped",[380]],[[380,380],"valid"],[[381,381],"mapped",[382]],[[382,382],"valid"],[[383,383],"mapped",[115]],[[384,384],"valid"],[[385,385],"mapped",[595]],[[386,386],"mapped",[387]],[[387,387],"valid"],[[388,388],"mapped",[389]],[[389,389],"valid"],[[390,390],"mapped",[596]],[[391,391],"mapped",[392]],[[392,392],"valid"],[[393,393],"mapped",[598]],[[394,394],"mapped",[599]],[[395,395],"mapped",[396]],[[396,397],"valid"],[[398,398],"mapped",[477]],[[399,399],"mapped",[601]],[[400,400],"mapped",[603]],[[401,401],"mapped",[402]],[[402,402],"valid"],[[403,403],"mapped",[608]],[[404,404],"mapped",[611]],[[405,405],"valid"],[[406,406],"mapped",[617]],[[407,407],"mapped",[616]],[[408,408],"mapped",[409]],[[409,411],"valid"],[[412,412],"mapped",[623]],[[413,413],"mapped",[626]],[[414,414],"valid"],[[415,415],"mapped",[629]],[[416,416],"mapped",[417]],[[417,417],"valid"],[[418,418],"mapped",[419]],[[419,419],"valid"],[[420,420],"mapped",[421]],[[421,421],"valid"],[[422,422],"mapped",[640]],[[423,423],"mapped",[424]],[[424,424],"valid"],[[425,425],"mapped",[643]],[[426,427],"valid"],[[428,428],"mapped",[429]],[[429,429],"valid"],[[430,430],"mapped",[648]],[[431,431],"mapped",[432]],[[432,432],"valid"],[[433,433],"mapped",[650]],[[434,434],"mapped",[651]],[[435,435],"mapped",[436]],[[436,436],"valid"],[[437,437],"mapped",[438]],[[438,438],"valid"],[[439,439],"mapped",[658]],[[440,440],"mapped",[441]],[[441,443],"valid"],[[444,444],"mapped",[445]],[[445,451],"valid"],[[452,454],"mapped",[100,382]],[[455,457],"mapped",[108,106]],[[458,460],"mapped",[110,106]],[[461,461],"mapped",[462]],[[462,462],"valid"],[[463,463],"mapped",[464]],[[464,464],"valid"],[[465,465],"mapped",[466]],[[466,466],"valid"],[[467,467],"mapped",[468]],[[468,468],"valid"],[[469,469],"mapped",[470]],[[470,470],"valid"],[[471,471],"mapped",[472]],[[472,472],"valid"],[[473,473],"mapped",[474]],[[474,474],"valid"],[[475,475],"mapped",[476]],[[476,477],"valid"],[[478,478],"mapped",[479]],[[479,479],"valid"],[[480,480],"mapped",[481]],[[481,481],"valid"],[[482,482],"mapped",[483]],[[483,483],"valid"],[[484,484],"mapped",[485]],[[485,485],"valid"],[[486,486],"mapped",[487]],[[487,487],"valid"],[[488,488],"mapped",[489]],[[489,489],"valid"],[[490,490],"mapped",[491]],[[491,491],"valid"],[[492,492],"mapped",[493]],[[493,493],"valid"],[[494,494],"mapped",[495]],[[495,496],"valid"],[[497,499],"mapped",[100,122]],[[500,500],"mapped",[501]],[[501,501],"valid"],[[502,502],"mapped",[405]],[[503,503],"mapped",[447]],[[504,504],"mapped",[505]],[[505,505],"valid"],[[506,506],"mapped",[507]],[[507,507],"valid"],[[508,508],"mapped",[509]],[[509,509],"valid"],[[510,510],"mapped",[511]],[[511,511],"valid"],[[512,512],"mapped",[513]],[[513,513],"valid"],[[514,514],"mapped",[515]],[[515,515],"valid"],[[516,516],"mapped",[517]],[[517,517],"valid"],[[518,518],"mapped",[519]],[[519,519],"valid"],[[520,520],"mapped",[521]],[[521,521],"valid"],[[522,522],"mapped",[523]],[[523,523],"valid"],[[524,524],"mapped",[525]],[[525,525],"valid"],[[526,526],"mapped",[527]],[[527,527],"valid"],[[528,528],"mapped",[529]],[[529,529],"valid"],[[530,530],"mapped",[531]],[[531,531],"valid"],[[532,532],"mapped",[533]],[[533,533],"valid"],[[534,534],"mapped",[535]],[[535,535],"valid"],[[536,536],"mapped",[537]],[[537,537],"valid"],[[538,538],"mapped",[539]],[[539,539],"valid"],[[540,540],"mapped",[541]],[[541,541],"valid"],[[542,542],"mapped",[543]],[[543,543],"valid"],[[544,544],"mapped",[414]],[[545,545],"valid"],[[546,546],"mapped",[547]],[[547,547],"valid"],[[548,548],"mapped",[549]],[[549,549],"valid"],[[550,550],"mapped",[551]],[[551,551],"valid"],[[552,552],"mapped",[553]],[[553,553],"valid"],[[554,554],"mapped",[555]],[[555,555],"valid"],[[556,556],"mapped",[557]],[[557,557],"valid"],[[558,558],"mapped",[559]],[[559,559],"valid"],[[560,560],"mapped",[561]],[[561,561],"valid"],[[562,562],"mapped",[563]],[[563,563],"valid"],[[564,566],"valid"],[[567,569],"valid"],[[570,570],"mapped",[11365]],[[571,571],"mapped",[572]],[[572,572],"valid"],[[573,573],"mapped",[410]],[[574,574],"mapped",[11366]],[[575,576],"valid"],[[577,577],"mapped",[578]],[[578,578],"valid"],[[579,579],"mapped",[384]],[[580,580],"mapped",[649]],[[581,581],"mapped",[652]],[[582,582],"mapped",[583]],[[583,583],"valid"],[[584,584],"mapped",[585]],[[585,585],"valid"],[[586,586],"mapped",[587]],[[587,587],"valid"],[[588,588],"mapped",[589]],[[589,589],"valid"],[[590,590],"mapped",[591]],[[591,591],"valid"],[[592,680],"valid"],[[681,685],"valid"],[[686,687],"valid"],[[688,688],"mapped",[104]],[[689,689],"mapped",[614]],[[690,690],"mapped",[106]],[[691,691],"mapped",[114]],[[692,692],"mapped",[633]],[[693,693],"mapped",[635]],[[694,694],"mapped",[641]],[[695,695],"mapped",[119]],[[696,696],"mapped",[121]],[[697,705],"valid"],[[706,709],"valid",[],"NV8"],[[710,721],"valid"],[[722,727],"valid",[],"NV8"],[[728,728],"disallowed_STD3_mapped",[32,774]],[[729,729],"disallowed_STD3_mapped",[32,775]],[[730,730],"disallowed_STD3_mapped",[32,778]],[[731,731],"disallowed_STD3_mapped",[32,808]],[[732,732],"disallowed_STD3_mapped",[32,771]],[[733,733],"disallowed_STD3_mapped",[32,779]],[[734,734],"valid",[],"NV8"],[[735,735],"valid",[],"NV8"],[[736,736],"mapped",[611]],[[737,737],"mapped",[108]],[[738,738],"mapped",[115]],[[739,739],"mapped",[120]],[[740,740],"mapped",[661]],[[741,745],"valid",[],"NV8"],[[746,747],"valid",[],"NV8"],[[748,748],"valid"],[[749,749],"valid",[],"NV8"],[[750,750],"valid"],[[751,767],"valid",[],"NV8"],[[768,831],"valid"],[[832,832],"mapped",[768]],[[833,833],"mapped",[769]],[[834,834],"valid"],[[835,835],"mapped",[787]],[[836,836],"mapped",[776,769]],[[837,837],"mapped",[953]],[[838,846],"valid"],[[847,847],"ignored"],[[848,855],"valid"],[[856,860],"valid"],[[861,863],"valid"],[[864,865],"valid"],[[866,866],"valid"],[[867,879],"valid"],[[880,880],"mapped",[881]],[[881,881],"valid"],[[882,882],"mapped",[883]],[[883,883],"valid"],[[884,884],"mapped",[697]],[[885,885],"valid"],[[886,886],"mapped",[887]],[[887,887],"valid"],[[888,889],"disallowed"],[[890,890],"disallowed_STD3_mapped",[32,953]],[[891,893],"valid"],[[894,894],"disallowed_STD3_mapped",[59]],[[895,895],"mapped",[1011]],[[896,899],"disallowed"],[[900,900],"disallowed_STD3_mapped",[32,769]],[[901,901],"disallowed_STD3_mapped",[32,776,769]],[[902,902],"mapped",[940]],[[903,903],"mapped",[183]],[[904,904],"mapped",[941]],[[905,905],"mapped",[942]],[[906,906],"mapped",[943]],[[907,907],"disallowed"],[[908,908],"mapped",[972]],[[909,909],"disallowed"],[[910,910],"mapped",[973]],[[911,911],"mapped",[974]],[[912,912],"valid"],[[913,913],"mapped",[945]],[[914,914],"mapped",[946]],[[915,915],"mapped",[947]],[[916,916],"mapped",[948]],[[917,917],"mapped",[949]],[[918,918],"mapped",[950]],[[919,919],"mapped",[951]],[[920,920],"mapped",[952]],[[921,921],"mapped",[953]],[[922,922],"mapped",[954]],[[923,923],"mapped",[955]],[[924,924],"mapped",[956]],[[925,925],"mapped",[957]],[[926,926],"mapped",[958]],[[927,927],"mapped",[959]],[[928,928],"mapped",[960]],[[929,929],"mapped",[961]],[[930,930],"disallowed"],[[931,931],"mapped",[963]],[[932,932],"mapped",[964]],[[933,933],"mapped",[965]],[[934,934],"mapped",[966]],[[935,935],"mapped",[967]],[[936,936],"mapped",[968]],[[937,937],"mapped",[969]],[[938,938],"mapped",[970]],[[939,939],"mapped",[971]],[[940,961],"valid"],[[962,962],"deviation",[963]],[[963,974],"valid"],[[975,975],"mapped",[983]],[[976,976],"mapped",[946]],[[977,977],"mapped",[952]],[[978,978],"mapped",[965]],[[979,979],"mapped",[973]],[[980,980],"mapped",[971]],[[981,981],"mapped",[966]],[[982,982],"mapped",[960]],[[983,983],"valid"],[[984,984],"mapped",[985]],[[985,985],"valid"],[[986,986],"mapped",[987]],[[987,987],"valid"],[[988,988],"mapped",[989]],[[989,989],"valid"],[[990,990],"mapped",[991]],[[991,991],"valid"],[[992,992],"mapped",[993]],[[993,993],"valid"],[[994,994],"mapped",[995]],[[995,995],"valid"],[[996,996],"mapped",[997]],[[997,997],"valid"],[[998,998],"mapped",[999]],[[999,999],"valid"],[[1000,1000],"mapped",[1001]],[[1001,1001],"valid"],[[1002,1002],"mapped",[1003]],[[1003,1003],"valid"],[[1004,1004],"mapped",[1005]],[[1005,1005],"valid"],[[1006,1006],"mapped",[1007]],[[1007,1007],"valid"],[[1008,1008],"mapped",[954]],[[1009,1009],"mapped",[961]],[[1010,1010],"mapped",[963]],[[1011,1011],"valid"],[[1012,1012],"mapped",[952]],[[1013,1013],"mapped",[949]],[[1014,1014],"valid",[],"NV8"],[[1015,1015],"mapped",[1016]],[[1016,1016],"valid"],[[1017,1017],"mapped",[963]],[[1018,1018],"mapped",[1019]],[[1019,1019],"valid"],[[1020,1020],"valid"],[[1021,1021],"mapped",[891]],[[1022,1022],"mapped",[892]],[[1023,1023],"mapped",[893]],[[1024,1024],"mapped",[1104]],[[1025,1025],"mapped",[1105]],[[1026,1026],"mapped",[1106]],[[1027,1027],"mapped",[1107]],[[1028,1028],"mapped",[1108]],[[1029,1029],"mapped",[1109]],[[1030,1030],"mapped",[1110]],[[1031,1031],"mapped",[1111]],[[1032,1032],"mapped",[1112]],[[1033,1033],"mapped",[1113]],[[1034,1034],"mapped",[1114]],[[1035,1035],"mapped",[1115]],[[1036,1036],"mapped",[1116]],[[1037,1037],"mapped",[1117]],[[1038,1038],"mapped",[1118]],[[1039,1039],"mapped",[1119]],[[1040,1040],"mapped",[1072]],[[1041,1041],"mapped",[1073]],[[1042,1042],"mapped",[1074]],[[1043,1043],"mapped",[1075]],[[1044,1044],"mapped",[1076]],[[1045,1045],"mapped",[1077]],[[1046,1046],"mapped",[1078]],[[1047,1047],"mapped",[1079]],[[1048,1048],"mapped",[1080]],[[1049,1049],"mapped",[1081]],[[1050,1050],"mapped",[1082]],[[1051,1051],"mapped",[1083]],[[1052,1052],"mapped",[1084]],[[1053,1053],"mapped",[1085]],[[1054,1054],"mapped",[1086]],[[1055,1055],"mapped",[1087]],[[1056,1056],"mapped",[1088]],[[1057,1057],"mapped",[1089]],[[1058,1058],"mapped",[1090]],[[1059,1059],"mapped",[1091]],[[1060,1060],"mapped",[1092]],[[1061,1061],"mapped",[1093]],[[1062,1062],"mapped",[1094]],[[1063,1063],"mapped",[1095]],[[1064,1064],"mapped",[1096]],[[1065,1065],"mapped",[1097]],[[1066,1066],"mapped",[1098]],[[1067,1067],"mapped",[1099]],[[1068,1068],"mapped",[1100]],[[1069,1069],"mapped",[1101]],[[1070,1070],"mapped",[1102]],[[1071,1071],"mapped",[1103]],[[1072,1103],"valid"],[[1104,1104],"valid"],[[1105,1116],"valid"],[[1117,1117],"valid"],[[1118,1119],"valid"],[[1120,1120],"mapped",[1121]],[[1121,1121],"valid"],[[1122,1122],"mapped",[1123]],[[1123,1123],"valid"],[[1124,1124],"mapped",[1125]],[[1125,1125],"valid"],[[1126,1126],"mapped",[1127]],[[1127,1127],"valid"],[[1128,1128],"mapped",[1129]],[[1129,1129],"valid"],[[1130,1130],"mapped",[1131]],[[1131,1131],"valid"],[[1132,1132],"mapped",[1133]],[[1133,1133],"valid"],[[1134,1134],"mapped",[1135]],[[1135,1135],"valid"],[[1136,1136],"mapped",[1137]],[[1137,1137],"valid"],[[1138,1138],"mapped",[1139]],[[1139,1139],"valid"],[[1140,1140],"mapped",[1141]],[[1141,1141],"valid"],[[1142,1142],"mapped",[1143]],[[1143,1143],"valid"],[[1144,1144],"mapped",[1145]],[[1145,1145],"valid"],[[1146,1146],"mapped",[1147]],[[1147,1147],"valid"],[[1148,1148],"mapped",[1149]],[[1149,1149],"valid"],[[1150,1150],"mapped",[1151]],[[1151,1151],"valid"],[[1152,1152],"mapped",[1153]],[[1153,1153],"valid"],[[1154,1154],"valid",[],"NV8"],[[1155,1158],"valid"],[[1159,1159],"valid"],[[1160,1161],"valid",[],"NV8"],[[1162,1162],"mapped",[1163]],[[1163,1163],"valid"],[[1164,1164],"mapped",[1165]],[[1165,1165],"valid"],[[1166,1166],"mapped",[1167]],[[1167,1167],"valid"],[[1168,1168],"mapped",[1169]],[[1169,1169],"valid"],[[1170,1170],"mapped",[1171]],[[1171,1171],"valid"],[[1172,1172],"mapped",[1173]],[[1173,1173],"valid"],[[1174,1174],"mapped",[1175]],[[1175,1175],"valid"],[[1176,1176],"mapped",[1177]],[[1177,1177],"valid"],[[1178,1178],"mapped",[1179]],[[1179,1179],"valid"],[[1180,1180],"mapped",[1181]],[[1181,1181],"valid"],[[1182,1182],"mapped",[1183]],[[1183,1183],"valid"],[[1184,1184],"mapped",[1185]],[[1185,1185],"valid"],[[1186,1186],"mapped",[1187]],[[1187,1187],"valid"],[[1188,1188],"mapped",[1189]],[[1189,1189],"valid"],[[1190,1190],"mapped",[1191]],[[1191,1191],"valid"],[[1192,1192],"mapped",[1193]],[[1193,1193],"valid"],[[1194,1194],"mapped",[1195]],[[1195,1195],"valid"],[[1196,1196],"mapped",[1197]],[[1197,1197],"valid"],[[1198,1198],"mapped",[1199]],[[1199,1199],"valid"],[[1200,1200],"mapped",[1201]],[[1201,1201],"valid"],[[1202,1202],"mapped",[1203]],[[1203,1203],"valid"],[[1204,1204],"mapped",[1205]],[[1205,1205],"valid"],[[1206,1206],"mapped",[1207]],[[1207,1207],"valid"],[[1208,1208],"mapped",[1209]],[[1209,1209],"valid"],[[1210,1210],"mapped",[1211]],[[1211,1211],"valid"],[[1212,1212],"mapped",[1213]],[[1213,1213],"valid"],[[1214,1214],"mapped",[1215]],[[1215,1215],"valid"],[[1216,1216],"disallowed"],[[1217,1217],"mapped",[1218]],[[1218,1218],"valid"],[[1219,1219],"mapped",[1220]],[[1220,1220],"valid"],[[1221,1221],"mapped",[1222]],[[1222,1222],"valid"],[[1223,1223],"mapped",[1224]],[[1224,1224],"valid"],[[1225,1225],"mapped",[1226]],[[1226,1226],"valid"],[[1227,1227],"mapped",[1228]],[[1228,1228],"valid"],[[1229,1229],"mapped",[1230]],[[1230,1230],"valid"],[[1231,1231],"valid"],[[1232,1232],"mapped",[1233]],[[1233,1233],"valid"],[[1234,1234],"mapped",[1235]],[[1235,1235],"valid"],[[1236,1236],"mapped",[1237]],[[1237,1237],"valid"],[[1238,1238],"mapped",[1239]],[[1239,1239],"valid"],[[1240,1240],"mapped",[1241]],[[1241,1241],"valid"],[[1242,1242],"mapped",[1243]],[[1243,1243],"valid"],[[1244,1244],"mapped",[1245]],[[1245,1245],"valid"],[[1246,1246],"mapped",[1247]],[[1247,1247],"valid"],[[1248,1248],"mapped",[1249]],[[1249,1249],"valid"],[[1250,1250],"mapped",[1251]],[[1251,1251],"valid"],[[1252,1252],"mapped",[1253]],[[1253,1253],"valid"],[[1254,1254],"mapped",[1255]],[[1255,1255],"valid"],[[1256,1256],"mapped",[1257]],[[1257,1257],"valid"],[[1258,1258],"mapped",[1259]],[[1259,1259],"valid"],[[1260,1260],"mapped",[1261]],[[1261,1261],"valid"],[[1262,1262],"mapped",[1263]],[[1263,1263],"valid"],[[1264,1264],"mapped",[1265]],[[1265,1265],"valid"],[[1266,1266],"mapped",[1267]],[[1267,1267],"valid"],[[1268,1268],"mapped",[1269]],[[1269,1269],"valid"],[[1270,1270],"mapped",[1271]],[[1271,1271],"valid"],[[1272,1272],"mapped",[1273]],[[1273,1273],"valid"],[[1274,1274],"mapped",[1275]],[[1275,1275],"valid"],[[1276,1276],"mapped",[1277]],[[1277,1277],"valid"],[[1278,1278],"mapped",[1279]],[[1279,1279],"valid"],[[1280,1280],"mapped",[1281]],[[1281,1281],"valid"],[[1282,1282],"mapped",[1283]],[[1283,1283],"valid"],[[1284,1284],"mapped",[1285]],[[1285,1285],"valid"],[[1286,1286],"mapped",[1287]],[[1287,1287],"valid"],[[1288,1288],"mapped",[1289]],[[1289,1289],"valid"],[[1290,1290],"mapped",[1291]],[[1291,1291],"valid"],[[1292,1292],"mapped",[1293]],[[1293,1293],"valid"],[[1294,1294],"mapped",[1295]],[[1295,1295],"valid"],[[1296,1296],"mapped",[1297]],[[1297,1297],"valid"],[[1298,1298],"mapped",[1299]],[[1299,1299],"valid"],[[1300,1300],"mapped",[1301]],[[1301,1301],"valid"],[[1302,1302],"mapped",[1303]],[[1303,1303],"valid"],[[1304,1304],"mapped",[1305]],[[1305,1305],"valid"],[[1306,1306],"mapped",[1307]],[[1307,1307],"valid"],[[1308,1308],"mapped",[1309]],[[1309,1309],"valid"],[[1310,1310],"mapped",[1311]],[[1311,1311],"valid"],[[1312,1312],"mapped",[1313]],[[1313,1313],"valid"],[[1314,1314],"mapped",[1315]],[[1315,1315],"valid"],[[1316,1316],"mapped",[1317]],[[1317,1317],"valid"],[[1318,1318],"mapped",[1319]],[[1319,1319],"valid"],[[1320,1320],"mapped",[1321]],[[1321,1321],"valid"],[[1322,1322],"mapped",[1323]],[[1323,1323],"valid"],[[1324,1324],"mapped",[1325]],[[1325,1325],"valid"],[[1326,1326],"mapped",[1327]],[[1327,1327],"valid"],[[1328,1328],"disallowed"],[[1329,1329],"mapped",[1377]],[[1330,1330],"mapped",[1378]],[[1331,1331],"mapped",[1379]],[[1332,1332],"mapped",[1380]],[[1333,1333],"mapped",[1381]],[[1334,1334],"mapped",[1382]],[[1335,1335],"mapped",[1383]],[[1336,1336],"mapped",[1384]],[[1337,1337],"mapped",[1385]],[[1338,1338],"mapped",[1386]],[[1339,1339],"mapped",[1387]],[[1340,1340],"mapped",[1388]],[[1341,1341],"mapped",[1389]],[[1342,1342],"mapped",[1390]],[[1343,1343],"mapped",[1391]],[[1344,1344],"mapped",[1392]],[[1345,1345],"mapped",[1393]],[[1346,1346],"mapped",[1394]],[[1347,1347],"mapped",[1395]],[[1348,1348],"mapped",[1396]],[[1349,1349],"mapped",[1397]],[[1350,1350],"mapped",[1398]],[[1351,1351],"mapped",[1399]],[[1352,1352],"mapped",[1400]],[[1353,1353],"mapped",[1401]],[[1354,1354],"mapped",[1402]],[[1355,1355],"mapped",[1403]],[[1356,1356],"mapped",[1404]],[[1357,1357],"mapped",[1405]],[[1358,1358],"mapped",[1406]],[[1359,1359],"mapped",[1407]],[[1360,1360],"mapped",[1408]],[[1361,1361],"mapped",[1409]],[[1362,1362],"mapped",[1410]],[[1363,1363],"mapped",[1411]],[[1364,1364],"mapped",[1412]],[[1365,1365],"mapped",[1413]],[[1366,1366],"mapped",[1414]],[[1367,1368],"disallowed"],[[1369,1369],"valid"],[[1370,1375],"valid",[],"NV8"],[[1376,1376],"disallowed"],[[1377,1414],"valid"],[[1415,1415],"mapped",[1381,1410]],[[1416,1416],"disallowed"],[[1417,1417],"valid",[],"NV8"],[[1418,1418],"valid",[],"NV8"],[[1419,1420],"disallowed"],[[1421,1422],"valid",[],"NV8"],[[1423,1423],"valid",[],"NV8"],[[1424,1424],"disallowed"],[[1425,1441],"valid"],[[1442,1442],"valid"],[[1443,1455],"valid"],[[1456,1465],"valid"],[[1466,1466],"valid"],[[1467,1469],"valid"],[[1470,1470],"valid",[],"NV8"],[[1471,1471],"valid"],[[1472,1472],"valid",[],"NV8"],[[1473,1474],"valid"],[[1475,1475],"valid",[],"NV8"],[[1476,1476],"valid"],[[1477,1477],"valid"],[[1478,1478],"valid",[],"NV8"],[[1479,1479],"valid"],[[1480,1487],"disallowed"],[[1488,1514],"valid"],[[1515,1519],"disallowed"],[[1520,1524],"valid"],[[1525,1535],"disallowed"],[[1536,1539],"disallowed"],[[1540,1540],"disallowed"],[[1541,1541],"disallowed"],[[1542,1546],"valid",[],"NV8"],[[1547,1547],"valid",[],"NV8"],[[1548,1548],"valid",[],"NV8"],[[1549,1551],"valid",[],"NV8"],[[1552,1557],"valid"],[[1558,1562],"valid"],[[1563,1563],"valid",[],"NV8"],[[1564,1564],"disallowed"],[[1565,1565],"disallowed"],[[1566,1566],"valid",[],"NV8"],[[1567,1567],"valid",[],"NV8"],[[1568,1568],"valid"],[[1569,1594],"valid"],[[1595,1599],"valid"],[[1600,1600],"valid",[],"NV8"],[[1601,1618],"valid"],[[1619,1621],"valid"],[[1622,1624],"valid"],[[1625,1630],"valid"],[[1631,1631],"valid"],[[1632,1641],"valid"],[[1642,1645],"valid",[],"NV8"],[[1646,1647],"valid"],[[1648,1652],"valid"],[[1653,1653],"mapped",[1575,1652]],[[1654,1654],"mapped",[1608,1652]],[[1655,1655],"mapped",[1735,1652]],[[1656,1656],"mapped",[1610,1652]],[[1657,1719],"valid"],[[1720,1721],"valid"],[[1722,1726],"valid"],[[1727,1727],"valid"],[[1728,1742],"valid"],[[1743,1743],"valid"],[[1744,1747],"valid"],[[1748,1748],"valid",[],"NV8"],[[1749,1756],"valid"],[[1757,1757],"disallowed"],[[1758,1758],"valid",[],"NV8"],[[1759,1768],"valid"],[[1769,1769],"valid",[],"NV8"],[[1770,1773],"valid"],[[1774,1775],"valid"],[[1776,1785],"valid"],[[1786,1790],"valid"],[[1791,1791],"valid"],[[1792,1805],"valid",[],"NV8"],[[1806,1806],"disallowed"],[[1807,1807],"disallowed"],[[1808,1836],"valid"],[[1837,1839],"valid"],[[1840,1866],"valid"],[[1867,1868],"disallowed"],[[1869,1871],"valid"],[[1872,1901],"valid"],[[1902,1919],"valid"],[[1920,1968],"valid"],[[1969,1969],"valid"],[[1970,1983],"disallowed"],[[1984,2037],"valid"],[[2038,2042],"valid",[],"NV8"],[[2043,2047],"disallowed"],[[2048,2093],"valid"],[[2094,2095],"disallowed"],[[2096,2110],"valid",[],"NV8"],[[2111,2111],"disallowed"],[[2112,2139],"valid"],[[2140,2141],"disallowed"],[[2142,2142],"valid",[],"NV8"],[[2143,2207],"disallowed"],[[2208,2208],"valid"],[[2209,2209],"valid"],[[2210,2220],"valid"],[[2221,2226],"valid"],[[2227,2228],"valid"],[[2229,2274],"disallowed"],[[2275,2275],"valid"],[[2276,2302],"valid"],[[2303,2303],"valid"],[[2304,2304],"valid"],[[2305,2307],"valid"],[[2308,2308],"valid"],[[2309,2361],"valid"],[[2362,2363],"valid"],[[2364,2381],"valid"],[[2382,2382],"valid"],[[2383,2383],"valid"],[[2384,2388],"valid"],[[2389,2389],"valid"],[[2390,2391],"valid"],[[2392,2392],"mapped",[2325,2364]],[[2393,2393],"mapped",[2326,2364]],[[2394,2394],"mapped",[2327,2364]],[[2395,2395],"mapped",[2332,2364]],[[2396,2396],"mapped",[2337,2364]],[[2397,2397],"mapped",[2338,2364]],[[2398,2398],"mapped",[2347,2364]],[[2399,2399],"mapped",[2351,2364]],[[2400,2403],"valid"],[[2404,2405],"valid",[],"NV8"],[[2406,2415],"valid"],[[2416,2416],"valid",[],"NV8"],[[2417,2418],"valid"],[[2419,2423],"valid"],[[2424,2424],"valid"],[[2425,2426],"valid"],[[2427,2428],"valid"],[[2429,2429],"valid"],[[2430,2431],"valid"],[[2432,2432],"valid"],[[2433,2435],"valid"],[[2436,2436],"disallowed"],[[2437,2444],"valid"],[[2445,2446],"disallowed"],[[2447,2448],"valid"],[[2449,2450],"disallowed"],[[2451,2472],"valid"],[[2473,2473],"disallowed"],[[2474,2480],"valid"],[[2481,2481],"disallowed"],[[2482,2482],"valid"],[[2483,2485],"disallowed"],[[2486,2489],"valid"],[[2490,2491],"disallowed"],[[2492,2492],"valid"],[[2493,2493],"valid"],[[2494,2500],"valid"],[[2501,2502],"disallowed"],[[2503,2504],"valid"],[[2505,2506],"disallowed"],[[2507,2509],"valid"],[[2510,2510],"valid"],[[2511,2518],"disallowed"],[[2519,2519],"valid"],[[2520,2523],"disallowed"],[[2524,2524],"mapped",[2465,2492]],[[2525,2525],"mapped",[2466,2492]],[[2526,2526],"disallowed"],[[2527,2527],"mapped",[2479,2492]],[[2528,2531],"valid"],[[2532,2533],"disallowed"],[[2534,2545],"valid"],[[2546,2554],"valid",[],"NV8"],[[2555,2555],"valid",[],"NV8"],[[2556,2560],"disallowed"],[[2561,2561],"valid"],[[2562,2562],"valid"],[[2563,2563],"valid"],[[2564,2564],"disallowed"],[[2565,2570],"valid"],[[2571,2574],"disallowed"],[[2575,2576],"valid"],[[2577,2578],"disallowed"],[[2579,2600],"valid"],[[2601,2601],"disallowed"],[[2602,2608],"valid"],[[2609,2609],"disallowed"],[[2610,2610],"valid"],[[2611,2611],"mapped",[2610,2620]],[[2612,2612],"disallowed"],[[2613,2613],"valid"],[[2614,2614],"mapped",[2616,2620]],[[2615,2615],"disallowed"],[[2616,2617],"valid"],[[2618,2619],"disallowed"],[[2620,2620],"valid"],[[2621,2621],"disallowed"],[[2622,2626],"valid"],[[2627,2630],"disallowed"],[[2631,2632],"valid"],[[2633,2634],"disallowed"],[[2635,2637],"valid"],[[2638,2640],"disallowed"],[[2641,2641],"valid"],[[2642,2648],"disallowed"],[[2649,2649],"mapped",[2582,2620]],[[2650,2650],"mapped",[2583,2620]],[[2651,2651],"mapped",[2588,2620]],[[2652,2652],"valid"],[[2653,2653],"disallowed"],[[2654,2654],"mapped",[2603,2620]],[[2655,2661],"disallowed"],[[2662,2676],"valid"],[[2677,2677],"valid"],[[2678,2688],"disallowed"],[[2689,2691],"valid"],[[2692,2692],"disallowed"],[[2693,2699],"valid"],[[2700,2700],"valid"],[[2701,2701],"valid"],[[2702,2702],"disallowed"],[[2703,2705],"valid"],[[2706,2706],"disallowed"],[[2707,2728],"valid"],[[2729,2729],"disallowed"],[[2730,2736],"valid"],[[2737,2737],"disallowed"],[[2738,2739],"valid"],[[2740,2740],"disallowed"],[[2741,2745],"valid"],[[2746,2747],"disallowed"],[[2748,2757],"valid"],[[2758,2758],"disallowed"],[[2759,2761],"valid"],[[2762,2762],"disallowed"],[[2763,2765],"valid"],[[2766,2767],"disallowed"],[[2768,2768],"valid"],[[2769,2783],"disallowed"],[[2784,2784],"valid"],[[2785,2787],"valid"],[[2788,2789],"disallowed"],[[2790,2799],"valid"],[[2800,2800],"valid",[],"NV8"],[[2801,2801],"valid",[],"NV8"],[[2802,2808],"disallowed"],[[2809,2809],"valid"],[[2810,2816],"disallowed"],[[2817,2819],"valid"],[[2820,2820],"disallowed"],[[2821,2828],"valid"],[[2829,2830],"disallowed"],[[2831,2832],"valid"],[[2833,2834],"disallowed"],[[2835,2856],"valid"],[[2857,2857],"disallowed"],[[2858,2864],"valid"],[[2865,2865],"disallowed"],[[2866,2867],"valid"],[[2868,2868],"disallowed"],[[2869,2869],"valid"],[[2870,2873],"valid"],[[2874,2875],"disallowed"],[[2876,2883],"valid"],[[2884,2884],"valid"],[[2885,2886],"disallowed"],[[2887,2888],"valid"],[[2889,2890],"disallowed"],[[2891,2893],"valid"],[[2894,2901],"disallowed"],[[2902,2903],"valid"],[[2904,2907],"disallowed"],[[2908,2908],"mapped",[2849,2876]],[[2909,2909],"mapped",[2850,2876]],[[2910,2910],"disallowed"],[[2911,2913],"valid"],[[2914,2915],"valid"],[[2916,2917],"disallowed"],[[2918,2927],"valid"],[[2928,2928],"valid",[],"NV8"],[[2929,2929],"valid"],[[2930,2935],"valid",[],"NV8"],[[2936,2945],"disallowed"],[[2946,2947],"valid"],[[2948,2948],"disallowed"],[[2949,2954],"valid"],[[2955,2957],"disallowed"],[[2958,2960],"valid"],[[2961,2961],"disallowed"],[[2962,2965],"valid"],[[2966,2968],"disallowed"],[[2969,2970],"valid"],[[2971,2971],"disallowed"],[[2972,2972],"valid"],[[2973,2973],"disallowed"],[[2974,2975],"valid"],[[2976,2978],"disallowed"],[[2979,2980],"valid"],[[2981,2983],"disallowed"],[[2984,2986],"valid"],[[2987,2989],"disallowed"],[[2990,2997],"valid"],[[2998,2998],"valid"],[[2999,3001],"valid"],[[3002,3005],"disallowed"],[[3006,3010],"valid"],[[3011,3013],"disallowed"],[[3014,3016],"valid"],[[3017,3017],"disallowed"],[[3018,3021],"valid"],[[3022,3023],"disallowed"],[[3024,3024],"valid"],[[3025,3030],"disallowed"],[[3031,3031],"valid"],[[3032,3045],"disallowed"],[[3046,3046],"valid"],[[3047,3055],"valid"],[[3056,3058],"valid",[],"NV8"],[[3059,3066],"valid",[],"NV8"],[[3067,3071],"disallowed"],[[3072,3072],"valid"],[[3073,3075],"valid"],[[3076,3076],"disallowed"],[[3077,3084],"valid"],[[3085,3085],"disallowed"],[[3086,3088],"valid"],[[3089,3089],"disallowed"],[[3090,3112],"valid"],[[3113,3113],"disallowed"],[[3114,3123],"valid"],[[3124,3124],"valid"],[[3125,3129],"valid"],[[3130,3132],"disallowed"],[[3133,3133],"valid"],[[3134,3140],"valid"],[[3141,3141],"disallowed"],[[3142,3144],"valid"],[[3145,3145],"disallowed"],[[3146,3149],"valid"],[[3150,3156],"disallowed"],[[3157,3158],"valid"],[[3159,3159],"disallowed"],[[3160,3161],"valid"],[[3162,3162],"valid"],[[3163,3167],"disallowed"],[[3168,3169],"valid"],[[3170,3171],"valid"],[[3172,3173],"disallowed"],[[3174,3183],"valid"],[[3184,3191],"disallowed"],[[3192,3199],"valid",[],"NV8"],[[3200,3200],"disallowed"],[[3201,3201],"valid"],[[3202,3203],"valid"],[[3204,3204],"disallowed"],[[3205,3212],"valid"],[[3213,3213],"disallowed"],[[3214,3216],"valid"],[[3217,3217],"disallowed"],[[3218,3240],"valid"],[[3241,3241],"disallowed"],[[3242,3251],"valid"],[[3252,3252],"disallowed"],[[3253,3257],"valid"],[[3258,3259],"disallowed"],[[3260,3261],"valid"],[[3262,3268],"valid"],[[3269,3269],"disallowed"],[[3270,3272],"valid"],[[3273,3273],"disallowed"],[[3274,3277],"valid"],[[3278,3284],"disallowed"],[[3285,3286],"valid"],[[3287,3293],"disallowed"],[[3294,3294],"valid"],[[3295,3295],"disallowed"],[[3296,3297],"valid"],[[3298,3299],"valid"],[[3300,3301],"disallowed"],[[3302,3311],"valid"],[[3312,3312],"disallowed"],[[3313,3314],"valid"],[[3315,3328],"disallowed"],[[3329,3329],"valid"],[[3330,3331],"valid"],[[3332,3332],"disallowed"],[[3333,3340],"valid"],[[3341,3341],"disallowed"],[[3342,3344],"valid"],[[3345,3345],"disallowed"],[[3346,3368],"valid"],[[3369,3369],"valid"],[[3370,3385],"valid"],[[3386,3386],"valid"],[[3387,3388],"disallowed"],[[3389,3389],"valid"],[[3390,3395],"valid"],[[3396,3396],"valid"],[[3397,3397],"disallowed"],[[3398,3400],"valid"],[[3401,3401],"disallowed"],[[3402,3405],"valid"],[[3406,3406],"valid"],[[3407,3414],"disallowed"],[[3415,3415],"valid"],[[3416,3422],"disallowed"],[[3423,3423],"valid"],[[3424,3425],"valid"],[[3426,3427],"valid"],[[3428,3429],"disallowed"],[[3430,3439],"valid"],[[3440,3445],"valid",[],"NV8"],[[3446,3448],"disallowed"],[[3449,3449],"valid",[],"NV8"],[[3450,3455],"valid"],[[3456,3457],"disallowed"],[[3458,3459],"valid"],[[3460,3460],"disallowed"],[[3461,3478],"valid"],[[3479,3481],"disallowed"],[[3482,3505],"valid"],[[3506,3506],"disallowed"],[[3507,3515],"valid"],[[3516,3516],"disallowed"],[[3517,3517],"valid"],[[3518,3519],"disallowed"],[[3520,3526],"valid"],[[3527,3529],"disallowed"],[[3530,3530],"valid"],[[3531,3534],"disallowed"],[[3535,3540],"valid"],[[3541,3541],"disallowed"],[[3542,3542],"valid"],[[3543,3543],"disallowed"],[[3544,3551],"valid"],[[3552,3557],"disallowed"],[[3558,3567],"valid"],[[3568,3569],"disallowed"],[[3570,3571],"valid"],[[3572,3572],"valid",[],"NV8"],[[3573,3584],"disallowed"],[[3585,3634],"valid"],[[3635,3635],"mapped",[3661,3634]],[[3636,3642],"valid"],[[3643,3646],"disallowed"],[[3647,3647],"valid",[],"NV8"],[[3648,3662],"valid"],[[3663,3663],"valid",[],"NV8"],[[3664,3673],"valid"],[[3674,3675],"valid",[],"NV8"],[[3676,3712],"disallowed"],[[3713,3714],"valid"],[[3715,3715],"disallowed"],[[3716,3716],"valid"],[[3717,3718],"disallowed"],[[3719,3720],"valid"],[[3721,3721],"disallowed"],[[3722,3722],"valid"],[[3723,3724],"disallowed"],[[3725,3725],"valid"],[[3726,3731],"disallowed"],[[3732,3735],"valid"],[[3736,3736],"disallowed"],[[3737,3743],"valid"],[[3744,3744],"disallowed"],[[3745,3747],"valid"],[[3748,3748],"disallowed"],[[3749,3749],"valid"],[[3750,3750],"disallowed"],[[3751,3751],"valid"],[[3752,3753],"disallowed"],[[3754,3755],"valid"],[[3756,3756],"disallowed"],[[3757,3762],"valid"],[[3763,3763],"mapped",[3789,3762]],[[3764,3769],"valid"],[[3770,3770],"disallowed"],[[3771,3773],"valid"],[[3774,3775],"disallowed"],[[3776,3780],"valid"],[[3781,3781],"disallowed"],[[3782,3782],"valid"],[[3783,3783],"disallowed"],[[3784,3789],"valid"],[[3790,3791],"disallowed"],[[3792,3801],"valid"],[[3802,3803],"disallowed"],[[3804,3804],"mapped",[3755,3737]],[[3805,3805],"mapped",[3755,3745]],[[3806,3807],"valid"],[[3808,3839],"disallowed"],[[3840,3840],"valid"],[[3841,3850],"valid",[],"NV8"],[[3851,3851],"valid"],[[3852,3852],"mapped",[3851]],[[3853,3863],"valid",[],"NV8"],[[3864,3865],"valid"],[[3866,3871],"valid",[],"NV8"],[[3872,3881],"valid"],[[3882,3892],"valid",[],"NV8"],[[3893,3893],"valid"],[[3894,3894],"valid",[],"NV8"],[[3895,3895],"valid"],[[3896,3896],"valid",[],"NV8"],[[3897,3897],"valid"],[[3898,3901],"valid",[],"NV8"],[[3902,3906],"valid"],[[3907,3907],"mapped",[3906,4023]],[[3908,3911],"valid"],[[3912,3912],"disallowed"],[[3913,3916],"valid"],[[3917,3917],"mapped",[3916,4023]],[[3918,3921],"valid"],[[3922,3922],"mapped",[3921,4023]],[[3923,3926],"valid"],[[3927,3927],"mapped",[3926,4023]],[[3928,3931],"valid"],[[3932,3932],"mapped",[3931,4023]],[[3933,3944],"valid"],[[3945,3945],"mapped",[3904,4021]],[[3946,3946],"valid"],[[3947,3948],"valid"],[[3949,3952],"disallowed"],[[3953,3954],"valid"],[[3955,3955],"mapped",[3953,3954]],[[3956,3956],"valid"],[[3957,3957],"mapped",[3953,3956]],[[3958,3958],"mapped",[4018,3968]],[[3959,3959],"mapped",[4018,3953,3968]],[[3960,3960],"mapped",[4019,3968]],[[3961,3961],"mapped",[4019,3953,3968]],[[3962,3968],"valid"],[[3969,3969],"mapped",[3953,3968]],[[3970,3972],"valid"],[[3973,3973],"valid",[],"NV8"],[[3974,3979],"valid"],[[3980,3983],"valid"],[[3984,3986],"valid"],[[3987,3987],"mapped",[3986,4023]],[[3988,3989],"valid"],[[3990,3990],"valid"],[[3991,3991],"valid"],[[3992,3992],"disallowed"],[[3993,3996],"valid"],[[3997,3997],"mapped",[3996,4023]],[[3998,4001],"valid"],[[4002,4002],"mapped",[4001,4023]],[[4003,4006],"valid"],[[4007,4007],"mapped",[4006,4023]],[[4008,4011],"valid"],[[4012,4012],"mapped",[4011,4023]],[[4013,4013],"valid"],[[4014,4016],"valid"],[[4017,4023],"valid"],[[4024,4024],"valid"],[[4025,4025],"mapped",[3984,4021]],[[4026,4028],"valid"],[[4029,4029],"disallowed"],[[4030,4037],"valid",[],"NV8"],[[4038,4038],"valid"],[[4039,4044],"valid",[],"NV8"],[[4045,4045],"disallowed"],[[4046,4046],"valid",[],"NV8"],[[4047,4047],"valid",[],"NV8"],[[4048,4049],"valid",[],"NV8"],[[4050,4052],"valid",[],"NV8"],[[4053,4056],"valid",[],"NV8"],[[4057,4058],"valid",[],"NV8"],[[4059,4095],"disallowed"],[[4096,4129],"valid"],[[4130,4130],"valid"],[[4131,4135],"valid"],[[4136,4136],"valid"],[[4137,4138],"valid"],[[4139,4139],"valid"],[[4140,4146],"valid"],[[4147,4149],"valid"],[[4150,4153],"valid"],[[4154,4159],"valid"],[[4160,4169],"valid"],[[4170,4175],"valid",[],"NV8"],[[4176,4185],"valid"],[[4186,4249],"valid"],[[4250,4253],"valid"],[[4254,4255],"valid",[],"NV8"],[[4256,4293],"disallowed"],[[4294,4294],"disallowed"],[[4295,4295],"mapped",[11559]],[[4296,4300],"disallowed"],[[4301,4301],"mapped",[11565]],[[4302,4303],"disallowed"],[[4304,4342],"valid"],[[4343,4344],"valid"],[[4345,4346],"valid"],[[4347,4347],"valid",[],"NV8"],[[4348,4348],"mapped",[4316]],[[4349,4351],"valid"],[[4352,4441],"valid",[],"NV8"],[[4442,4446],"valid",[],"NV8"],[[4447,4448],"disallowed"],[[4449,4514],"valid",[],"NV8"],[[4515,4519],"valid",[],"NV8"],[[4520,4601],"valid",[],"NV8"],[[4602,4607],"valid",[],"NV8"],[[4608,4614],"valid"],[[4615,4615],"valid"],[[4616,4678],"valid"],[[4679,4679],"valid"],[[4680,4680],"valid"],[[4681,4681],"disallowed"],[[4682,4685],"valid"],[[4686,4687],"disallowed"],[[4688,4694],"valid"],[[4695,4695],"disallowed"],[[4696,4696],"valid"],[[4697,4697],"disallowed"],[[4698,4701],"valid"],[[4702,4703],"disallowed"],[[4704,4742],"valid"],[[4743,4743],"valid"],[[4744,4744],"valid"],[[4745,4745],"disallowed"],[[4746,4749],"valid"],[[4750,4751],"disallowed"],[[4752,4782],"valid"],[[4783,4783],"valid"],[[4784,4784],"valid"],[[4785,4785],"disallowed"],[[4786,4789],"valid"],[[4790,4791],"disallowed"],[[4792,4798],"valid"],[[4799,4799],"disallowed"],[[4800,4800],"valid"],[[4801,4801],"disallowed"],[[4802,4805],"valid"],[[4806,4807],"disallowed"],[[4808,4814],"valid"],[[4815,4815],"valid"],[[4816,4822],"valid"],[[4823,4823],"disallowed"],[[4824,4846],"valid"],[[4847,4847],"valid"],[[4848,4878],"valid"],[[4879,4879],"valid"],[[4880,4880],"valid"],[[4881,4881],"disallowed"],[[4882,4885],"valid"],[[4886,4887],"disallowed"],[[4888,4894],"valid"],[[4895,4895],"valid"],[[4896,4934],"valid"],[[4935,4935],"valid"],[[4936,4954],"valid"],[[4955,4956],"disallowed"],[[4957,4958],"valid"],[[4959,4959],"valid"],[[4960,4960],"valid",[],"NV8"],[[4961,4988],"valid",[],"NV8"],[[4989,4991],"disallowed"],[[4992,5007],"valid"],[[5008,5017],"valid",[],"NV8"],[[5018,5023],"disallowed"],[[5024,5108],"valid"],[[5109,5109],"valid"],[[5110,5111],"disallowed"],[[5112,5112],"mapped",[5104]],[[5113,5113],"mapped",[5105]],[[5114,5114],"mapped",[5106]],[[5115,5115],"mapped",[5107]],[[5116,5116],"mapped",[5108]],[[5117,5117],"mapped",[5109]],[[5118,5119],"disallowed"],[[5120,5120],"valid",[],"NV8"],[[5121,5740],"valid"],[[5741,5742],"valid",[],"NV8"],[[5743,5750],"valid"],[[5751,5759],"valid"],[[5760,5760],"disallowed"],[[5761,5786],"valid"],[[5787,5788],"valid",[],"NV8"],[[5789,5791],"disallowed"],[[5792,5866],"valid"],[[5867,5872],"valid",[],"NV8"],[[5873,5880],"valid"],[[5881,5887],"disallowed"],[[5888,5900],"valid"],[[5901,5901],"disallowed"],[[5902,5908],"valid"],[[5909,5919],"disallowed"],[[5920,5940],"valid"],[[5941,5942],"valid",[],"NV8"],[[5943,5951],"disallowed"],[[5952,5971],"valid"],[[5972,5983],"disallowed"],[[5984,5996],"valid"],[[5997,5997],"disallowed"],[[5998,6000],"valid"],[[6001,6001],"disallowed"],[[6002,6003],"valid"],[[6004,6015],"disallowed"],[[6016,6067],"valid"],[[6068,6069],"disallowed"],[[6070,6099],"valid"],[[6100,6102],"valid",[],"NV8"],[[6103,6103],"valid"],[[6104,6107],"valid",[],"NV8"],[[6108,6108],"valid"],[[6109,6109],"valid"],[[6110,6111],"disallowed"],[[6112,6121],"valid"],[[6122,6127],"disallowed"],[[6128,6137],"valid",[],"NV8"],[[6138,6143],"disallowed"],[[6144,6149],"valid",[],"NV8"],[[6150,6150],"disallowed"],[[6151,6154],"valid",[],"NV8"],[[6155,6157],"ignored"],[[6158,6158],"disallowed"],[[6159,6159],"disallowed"],[[6160,6169],"valid"],[[6170,6175],"disallowed"],[[6176,6263],"valid"],[[6264,6271],"disallowed"],[[6272,6313],"valid"],[[6314,6314],"valid"],[[6315,6319],"disallowed"],[[6320,6389],"valid"],[[6390,6399],"disallowed"],[[6400,6428],"valid"],[[6429,6430],"valid"],[[6431,6431],"disallowed"],[[6432,6443],"valid"],[[6444,6447],"disallowed"],[[6448,6459],"valid"],[[6460,6463],"disallowed"],[[6464,6464],"valid",[],"NV8"],[[6465,6467],"disallowed"],[[6468,6469],"valid",[],"NV8"],[[6470,6509],"valid"],[[6510,6511],"disallowed"],[[6512,6516],"valid"],[[6517,6527],"disallowed"],[[6528,6569],"valid"],[[6570,6571],"valid"],[[6572,6575],"disallowed"],[[6576,6601],"valid"],[[6602,6607],"disallowed"],[[6608,6617],"valid"],[[6618,6618],"valid",[],"XV8"],[[6619,6621],"disallowed"],[[6622,6623],"valid",[],"NV8"],[[6624,6655],"valid",[],"NV8"],[[6656,6683],"valid"],[[6684,6685],"disallowed"],[[6686,6687],"valid",[],"NV8"],[[6688,6750],"valid"],[[6751,6751],"disallowed"],[[6752,6780],"valid"],[[6781,6782],"disallowed"],[[6783,6793],"valid"],[[6794,6799],"disallowed"],[[6800,6809],"valid"],[[6810,6815],"disallowed"],[[6816,6822],"valid",[],"NV8"],[[6823,6823],"valid"],[[6824,6829],"valid",[],"NV8"],[[6830,6831],"disallowed"],[[6832,6845],"valid"],[[6846,6846],"valid",[],"NV8"],[[6847,6911],"disallowed"],[[6912,6987],"valid"],[[6988,6991],"disallowed"],[[6992,7001],"valid"],[[7002,7018],"valid",[],"NV8"],[[7019,7027],"valid"],[[7028,7036],"valid",[],"NV8"],[[7037,7039],"disallowed"],[[7040,7082],"valid"],[[7083,7085],"valid"],[[7086,7097],"valid"],[[7098,7103],"valid"],[[7104,7155],"valid"],[[7156,7163],"disallowed"],[[7164,7167],"valid",[],"NV8"],[[7168,7223],"valid"],[[7224,7226],"disallowed"],[[7227,7231],"valid",[],"NV8"],[[7232,7241],"valid"],[[7242,7244],"disallowed"],[[7245,7293],"valid"],[[7294,7295],"valid",[],"NV8"],[[7296,7359],"disallowed"],[[7360,7367],"valid",[],"NV8"],[[7368,7375],"disallowed"],[[7376,7378],"valid"],[[7379,7379],"valid",[],"NV8"],[[7380,7410],"valid"],[[7411,7414],"valid"],[[7415,7415],"disallowed"],[[7416,7417],"valid"],[[7418,7423],"disallowed"],[[7424,7467],"valid"],[[7468,7468],"mapped",[97]],[[7469,7469],"mapped",[230]],[[7470,7470],"mapped",[98]],[[7471,7471],"valid"],[[7472,7472],"mapped",[100]],[[7473,7473],"mapped",[101]],[[7474,7474],"mapped",[477]],[[7475,7475],"mapped",[103]],[[7476,7476],"mapped",[104]],[[7477,7477],"mapped",[105]],[[7478,7478],"mapped",[106]],[[7479,7479],"mapped",[107]],[[7480,7480],"mapped",[108]],[[7481,7481],"mapped",[109]],[[7482,7482],"mapped",[110]],[[7483,7483],"valid"],[[7484,7484],"mapped",[111]],[[7485,7485],"mapped",[547]],[[7486,7486],"mapped",[112]],[[7487,7487],"mapped",[114]],[[7488,7488],"mapped",[116]],[[7489,7489],"mapped",[117]],[[7490,7490],"mapped",[119]],[[7491,7491],"mapped",[97]],[[7492,7492],"mapped",[592]],[[7493,7493],"mapped",[593]],[[7494,7494],"mapped",[7426]],[[7495,7495],"mapped",[98]],[[7496,7496],"mapped",[100]],[[7497,7497],"mapped",[101]],[[7498,7498],"mapped",[601]],[[7499,7499],"mapped",[603]],[[7500,7500],"mapped",[604]],[[7501,7501],"mapped",[103]],[[7502,7502],"valid"],[[7503,7503],"mapped",[107]],[[7504,7504],"mapped",[109]],[[7505,7505],"mapped",[331]],[[7506,7506],"mapped",[111]],[[7507,7507],"mapped",[596]],[[7508,7508],"mapped",[7446]],[[7509,7509],"mapped",[7447]],[[7510,7510],"mapped",[112]],[[7511,7511],"mapped",[116]],[[7512,7512],"mapped",[117]],[[7513,7513],"mapped",[7453]],[[7514,7514],"mapped",[623]],[[7515,7515],"mapped",[118]],[[7516,7516],"mapped",[7461]],[[7517,7517],"mapped",[946]],[[7518,7518],"mapped",[947]],[[7519,7519],"mapped",[948]],[[7520,7520],"mapped",[966]],[[7521,7521],"mapped",[967]],[[7522,7522],"mapped",[105]],[[7523,7523],"mapped",[114]],[[7524,7524],"mapped",[117]],[[7525,7525],"mapped",[118]],[[7526,7526],"mapped",[946]],[[7527,7527],"mapped",[947]],[[7528,7528],"mapped",[961]],[[7529,7529],"mapped",[966]],[[7530,7530],"mapped",[967]],[[7531,7531],"valid"],[[7532,7543],"valid"],[[7544,7544],"mapped",[1085]],[[7545,7578],"valid"],[[7579,7579],"mapped",[594]],[[7580,7580],"mapped",[99]],[[7581,7581],"mapped",[597]],[[7582,7582],"mapped",[240]],[[7583,7583],"mapped",[604]],[[7584,7584],"mapped",[102]],[[7585,7585],"mapped",[607]],[[7586,7586],"mapped",[609]],[[7587,7587],"mapped",[613]],[[7588,7588],"mapped",[616]],[[7589,7589],"mapped",[617]],[[7590,7590],"mapped",[618]],[[7591,7591],"mapped",[7547]],[[7592,7592],"mapped",[669]],[[7593,7593],"mapped",[621]],[[7594,7594],"mapped",[7557]],[[7595,7595],"mapped",[671]],[[7596,7596],"mapped",[625]],[[7597,7597],"mapped",[624]],[[7598,7598],"mapped",[626]],[[7599,7599],"mapped",[627]],[[7600,7600],"mapped",[628]],[[7601,7601],"mapped",[629]],[[7602,7602],"mapped",[632]],[[7603,7603],"mapped",[642]],[[7604,7604],"mapped",[643]],[[7605,7605],"mapped",[427]],[[7606,7606],"mapped",[649]],[[7607,7607],"mapped",[650]],[[7608,7608],"mapped",[7452]],[[7609,7609],"mapped",[651]],[[7610,7610],"mapped",[652]],[[7611,7611],"mapped",[122]],[[7612,7612],"mapped",[656]],[[7613,7613],"mapped",[657]],[[7614,7614],"mapped",[658]],[[7615,7615],"mapped",[952]],[[7616,7619],"valid"],[[7620,7626],"valid"],[[7627,7654],"valid"],[[7655,7669],"valid"],[[7670,7675],"disallowed"],[[7676,7676],"valid"],[[7677,7677],"valid"],[[7678,7679],"valid"],[[7680,7680],"mapped",[7681]],[[7681,7681],"valid"],[[7682,7682],"mapped",[7683]],[[7683,7683],"valid"],[[7684,7684],"mapped",[7685]],[[7685,7685],"valid"],[[7686,7686],"mapped",[7687]],[[7687,7687],"valid"],[[7688,7688],"mapped",[7689]],[[7689,7689],"valid"],[[7690,7690],"mapped",[7691]],[[7691,7691],"valid"],[[7692,7692],"mapped",[7693]],[[7693,7693],"valid"],[[7694,7694],"mapped",[7695]],[[7695,7695],"valid"],[[7696,7696],"mapped",[7697]],[[7697,7697],"valid"],[[7698,7698],"mapped",[7699]],[[7699,7699],"valid"],[[7700,7700],"mapped",[7701]],[[7701,7701],"valid"],[[7702,7702],"mapped",[7703]],[[7703,7703],"valid"],[[7704,7704],"mapped",[7705]],[[7705,7705],"valid"],[[7706,7706],"mapped",[7707]],[[7707,7707],"valid"],[[7708,7708],"mapped",[7709]],[[7709,7709],"valid"],[[7710,7710],"mapped",[7711]],[[7711,7711],"valid"],[[7712,7712],"mapped",[7713]],[[7713,7713],"valid"],[[7714,7714],"mapped",[7715]],[[7715,7715],"valid"],[[7716,7716],"mapped",[7717]],[[7717,7717],"valid"],[[7718,7718],"mapped",[7719]],[[7719,7719],"valid"],[[7720,7720],"mapped",[7721]],[[7721,7721],"valid"],[[7722,7722],"mapped",[7723]],[[7723,7723],"valid"],[[7724,7724],"mapped",[7725]],[[7725,7725],"valid"],[[7726,7726],"mapped",[7727]],[[7727,7727],"valid"],[[7728,7728],"mapped",[7729]],[[7729,7729],"valid"],[[7730,7730],"mapped",[7731]],[[7731,7731],"valid"],[[7732,7732],"mapped",[7733]],[[7733,7733],"valid"],[[7734,7734],"mapped",[7735]],[[7735,7735],"valid"],[[7736,7736],"mapped",[7737]],[[7737,7737],"valid"],[[7738,7738],"mapped",[7739]],[[7739,7739],"valid"],[[7740,7740],"mapped",[7741]],[[7741,7741],"valid"],[[7742,7742],"mapped",[7743]],[[7743,7743],"valid"],[[7744,7744],"mapped",[7745]],[[7745,7745],"valid"],[[7746,7746],"mapped",[7747]],[[7747,7747],"valid"],[[7748,7748],"mapped",[7749]],[[7749,7749],"valid"],[[7750,7750],"mapped",[7751]],[[7751,7751],"valid"],[[7752,7752],"mapped",[7753]],[[7753,7753],"valid"],[[7754,7754],"mapped",[7755]],[[7755,7755],"valid"],[[7756,7756],"mapped",[7757]],[[7757,7757],"valid"],[[7758,7758],"mapped",[7759]],[[7759,7759],"valid"],[[7760,7760],"mapped",[7761]],[[7761,7761],"valid"],[[7762,7762],"mapped",[7763]],[[7763,7763],"valid"],[[7764,7764],"mapped",[7765]],[[7765,7765],"valid"],[[7766,7766],"mapped",[7767]],[[7767,7767],"valid"],[[7768,7768],"mapped",[7769]],[[7769,7769],"valid"],[[7770,7770],"mapped",[7771]],[[7771,7771],"valid"],[[7772,7772],"mapped",[7773]],[[7773,7773],"valid"],[[7774,7774],"mapped",[7775]],[[7775,7775],"valid"],[[7776,7776],"mapped",[7777]],[[7777,7777],"valid"],[[7778,7778],"mapped",[7779]],[[7779,7779],"valid"],[[7780,7780],"mapped",[7781]],[[7781,7781],"valid"],[[7782,7782],"mapped",[7783]],[[7783,7783],"valid"],[[7784,7784],"mapped",[7785]],[[7785,7785],"valid"],[[7786,7786],"mapped",[7787]],[[7787,7787],"valid"],[[7788,7788],"mapped",[7789]],[[7789,7789],"valid"],[[7790,7790],"mapped",[7791]],[[7791,7791],"valid"],[[7792,7792],"mapped",[7793]],[[7793,7793],"valid"],[[7794,7794],"mapped",[7795]],[[7795,7795],"valid"],[[7796,7796],"mapped",[7797]],[[7797,7797],"valid"],[[7798,7798],"mapped",[7799]],[[7799,7799],"valid"],[[7800,7800],"mapped",[7801]],[[7801,7801],"valid"],[[7802,7802],"mapped",[7803]],[[7803,7803],"valid"],[[7804,7804],"mapped",[7805]],[[7805,7805],"valid"],[[7806,7806],"mapped",[7807]],[[7807,7807],"valid"],[[7808,7808],"mapped",[7809]],[[7809,7809],"valid"],[[7810,7810],"mapped",[7811]],[[7811,7811],"valid"],[[7812,7812],"mapped",[7813]],[[7813,7813],"valid"],[[7814,7814],"mapped",[7815]],[[7815,7815],"valid"],[[7816,7816],"mapped",[7817]],[[7817,7817],"valid"],[[7818,7818],"mapped",[7819]],[[7819,7819],"valid"],[[7820,7820],"mapped",[7821]],[[7821,7821],"valid"],[[7822,7822],"mapped",[7823]],[[7823,7823],"valid"],[[7824,7824],"mapped",[7825]],[[7825,7825],"valid"],[[7826,7826],"mapped",[7827]],[[7827,7827],"valid"],[[7828,7828],"mapped",[7829]],[[7829,7833],"valid"],[[7834,7834],"mapped",[97,702]],[[7835,7835],"mapped",[7777]],[[7836,7837],"valid"],[[7838,7838],"mapped",[115,115]],[[7839,7839],"valid"],[[7840,7840],"mapped",[7841]],[[7841,7841],"valid"],[[7842,7842],"mapped",[7843]],[[7843,7843],"valid"],[[7844,7844],"mapped",[7845]],[[7845,7845],"valid"],[[7846,7846],"mapped",[7847]],[[7847,7847],"valid"],[[7848,7848],"mapped",[7849]],[[7849,7849],"valid"],[[7850,7850],"mapped",[7851]],[[7851,7851],"valid"],[[7852,7852],"mapped",[7853]],[[7853,7853],"valid"],[[7854,7854],"mapped",[7855]],[[7855,7855],"valid"],[[7856,7856],"mapped",[7857]],[[7857,7857],"valid"],[[7858,7858],"mapped",[7859]],[[7859,7859],"valid"],[[7860,7860],"mapped",[7861]],[[7861,7861],"valid"],[[7862,7862],"mapped",[7863]],[[7863,7863],"valid"],[[7864,7864],"mapped",[7865]],[[7865,7865],"valid"],[[7866,7866],"mapped",[7867]],[[7867,7867],"valid"],[[7868,7868],"mapped",[7869]],[[7869,7869],"valid"],[[7870,7870],"mapped",[7871]],[[7871,7871],"valid"],[[7872,7872],"mapped",[7873]],[[7873,7873],"valid"],[[7874,7874],"mapped",[7875]],[[7875,7875],"valid"],[[7876,7876],"mapped",[7877]],[[7877,7877],"valid"],[[7878,7878],"mapped",[7879]],[[7879,7879],"valid"],[[7880,7880],"mapped",[7881]],[[7881,7881],"valid"],[[7882,7882],"mapped",[7883]],[[7883,7883],"valid"],[[7884,7884],"mapped",[7885]],[[7885,7885],"valid"],[[7886,7886],"mapped",[7887]],[[7887,7887],"valid"],[[7888,7888],"mapped",[7889]],[[7889,7889],"valid"],[[7890,7890],"mapped",[7891]],[[7891,7891],"valid"],[[7892,7892],"mapped",[7893]],[[7893,7893],"valid"],[[7894,7894],"mapped",[7895]],[[7895,7895],"valid"],[[7896,7896],"mapped",[7897]],[[7897,7897],"valid"],[[7898,7898],"mapped",[7899]],[[7899,7899],"valid"],[[7900,7900],"mapped",[7901]],[[7901,7901],"valid"],[[7902,7902],"mapped",[7903]],[[7903,7903],"valid"],[[7904,7904],"mapped",[7905]],[[7905,7905],"valid"],[[7906,7906],"mapped",[7907]],[[7907,7907],"valid"],[[7908,7908],"mapped",[7909]],[[7909,7909],"valid"],[[7910,7910],"mapped",[7911]],[[7911,7911],"valid"],[[7912,7912],"mapped",[7913]],[[7913,7913],"valid"],[[7914,7914],"mapped",[7915]],[[7915,7915],"valid"],[[7916,7916],"mapped",[7917]],[[7917,7917],"valid"],[[7918,7918],"mapped",[7919]],[[7919,7919],"valid"],[[7920,7920],"mapped",[7921]],[[7921,7921],"valid"],[[7922,7922],"mapped",[7923]],[[7923,7923],"valid"],[[7924,7924],"mapped",[7925]],[[7925,7925],"valid"],[[7926,7926],"mapped",[7927]],[[7927,7927],"valid"],[[7928,7928],"mapped",[7929]],[[7929,7929],"valid"],[[7930,7930],"mapped",[7931]],[[7931,7931],"valid"],[[7932,7932],"mapped",[7933]],[[7933,7933],"valid"],[[7934,7934],"mapped",[7935]],[[7935,7935],"valid"],[[7936,7943],"valid"],[[7944,7944],"mapped",[7936]],[[7945,7945],"mapped",[7937]],[[7946,7946],"mapped",[7938]],[[7947,7947],"mapped",[7939]],[[7948,7948],"mapped",[7940]],[[7949,7949],"mapped",[7941]],[[7950,7950],"mapped",[7942]],[[7951,7951],"mapped",[7943]],[[7952,7957],"valid"],[[7958,7959],"disallowed"],[[7960,7960],"mapped",[7952]],[[7961,7961],"mapped",[7953]],[[7962,7962],"mapped",[7954]],[[7963,7963],"mapped",[7955]],[[7964,7964],"mapped",[7956]],[[7965,7965],"mapped",[7957]],[[7966,7967],"disallowed"],[[7968,7975],"valid"],[[7976,7976],"mapped",[7968]],[[7977,7977],"mapped",[7969]],[[7978,7978],"mapped",[7970]],[[7979,7979],"mapped",[7971]],[[7980,7980],"mapped",[7972]],[[7981,7981],"mapped",[7973]],[[7982,7982],"mapped",[7974]],[[7983,7983],"mapped",[7975]],[[7984,7991],"valid"],[[7992,7992],"mapped",[7984]],[[7993,7993],"mapped",[7985]],[[7994,7994],"mapped",[7986]],[[7995,7995],"mapped",[7987]],[[7996,7996],"mapped",[7988]],[[7997,7997],"mapped",[7989]],[[7998,7998],"mapped",[7990]],[[7999,7999],"mapped",[7991]],[[8000,8005],"valid"],[[8006,8007],"disallowed"],[[8008,8008],"mapped",[8000]],[[8009,8009],"mapped",[8001]],[[8010,8010],"mapped",[8002]],[[8011,8011],"mapped",[8003]],[[8012,8012],"mapped",[8004]],[[8013,8013],"mapped",[8005]],[[8014,8015],"disallowed"],[[8016,8023],"valid"],[[8024,8024],"disallowed"],[[8025,8025],"mapped",[8017]],[[8026,8026],"disallowed"],[[8027,8027],"mapped",[8019]],[[8028,8028],"disallowed"],[[8029,8029],"mapped",[8021]],[[8030,8030],"disallowed"],[[8031,8031],"mapped",[8023]],[[8032,8039],"valid"],[[8040,8040],"mapped",[8032]],[[8041,8041],"mapped",[8033]],[[8042,8042],"mapped",[8034]],[[8043,8043],"mapped",[8035]],[[8044,8044],"mapped",[8036]],[[8045,8045],"mapped",[8037]],[[8046,8046],"mapped",[8038]],[[8047,8047],"mapped",[8039]],[[8048,8048],"valid"],[[8049,8049],"mapped",[940]],[[8050,8050],"valid"],[[8051,8051],"mapped",[941]],[[8052,8052],"valid"],[[8053,8053],"mapped",[942]],[[8054,8054],"valid"],[[8055,8055],"mapped",[943]],[[8056,8056],"valid"],[[8057,8057],"mapped",[972]],[[8058,8058],"valid"],[[8059,8059],"mapped",[973]],[[8060,8060],"valid"],[[8061,8061],"mapped",[974]],[[8062,8063],"disallowed"],[[8064,8064],"mapped",[7936,953]],[[8065,8065],"mapped",[7937,953]],[[8066,8066],"mapped",[7938,953]],[[8067,8067],"mapped",[7939,953]],[[8068,8068],"mapped",[7940,953]],[[8069,8069],"mapped",[7941,953]],[[8070,8070],"mapped",[7942,953]],[[8071,8071],"mapped",[7943,953]],[[8072,8072],"mapped",[7936,953]],[[8073,8073],"mapped",[7937,953]],[[8074,8074],"mapped",[7938,953]],[[8075,8075],"mapped",[7939,953]],[[8076,8076],"mapped",[7940,953]],[[8077,8077],"mapped",[7941,953]],[[8078,8078],"mapped",[7942,953]],[[8079,8079],"mapped",[7943,953]],[[8080,8080],"mapped",[7968,953]],[[8081,8081],"mapped",[7969,953]],[[8082,8082],"mapped",[7970,953]],[[8083,8083],"mapped",[7971,953]],[[8084,8084],"mapped",[7972,953]],[[8085,8085],"mapped",[7973,953]],[[8086,8086],"mapped",[7974,953]],[[8087,8087],"mapped",[7975,953]],[[8088,8088],"mapped",[7968,953]],[[8089,8089],"mapped",[7969,953]],[[8090,8090],"mapped",[7970,953]],[[8091,8091],"mapped",[7971,953]],[[8092,8092],"mapped",[7972,953]],[[8093,8093],"mapped",[7973,953]],[[8094,8094],"mapped",[7974,953]],[[8095,8095],"mapped",[7975,953]],[[8096,8096],"mapped",[8032,953]],[[8097,8097],"mapped",[8033,953]],[[8098,8098],"mapped",[8034,953]],[[8099,8099],"mapped",[8035,953]],[[8100,8100],"mapped",[8036,953]],[[8101,8101],"mapped",[8037,953]],[[8102,8102],"mapped",[8038,953]],[[8103,8103],"mapped",[8039,953]],[[8104,8104],"mapped",[8032,953]],[[8105,8105],"mapped",[8033,953]],[[8106,8106],"mapped",[8034,953]],[[8107,8107],"mapped",[8035,953]],[[8108,8108],"mapped",[8036,953]],[[8109,8109],"mapped",[8037,953]],[[8110,8110],"mapped",[8038,953]],[[8111,8111],"mapped",[8039,953]],[[8112,8113],"valid"],[[8114,8114],"mapped",[8048,953]],[[8115,8115],"mapped",[945,953]],[[8116,8116],"mapped",[940,953]],[[8117,8117],"disallowed"],[[8118,8118],"valid"],[[8119,8119],"mapped",[8118,953]],[[8120,8120],"mapped",[8112]],[[8121,8121],"mapped",[8113]],[[8122,8122],"mapped",[8048]],[[8123,8123],"mapped",[940]],[[8124,8124],"mapped",[945,953]],[[8125,8125],"disallowed_STD3_mapped",[32,787]],[[8126,8126],"mapped",[953]],[[8127,8127],"disallowed_STD3_mapped",[32,787]],[[8128,8128],"disallowed_STD3_mapped",[32,834]],[[8129,8129],"disallowed_STD3_mapped",[32,776,834]],[[8130,8130],"mapped",[8052,953]],[[8131,8131],"mapped",[951,953]],[[8132,8132],"mapped",[942,953]],[[8133,8133],"disallowed"],[[8134,8134],"valid"],[[8135,8135],"mapped",[8134,953]],[[8136,8136],"mapped",[8050]],[[8137,8137],"mapped",[941]],[[8138,8138],"mapped",[8052]],[[8139,8139],"mapped",[942]],[[8140,8140],"mapped",[951,953]],[[8141,8141],"disallowed_STD3_mapped",[32,787,768]],[[8142,8142],"disallowed_STD3_mapped",[32,787,769]],[[8143,8143],"disallowed_STD3_mapped",[32,787,834]],[[8144,8146],"valid"],[[8147,8147],"mapped",[912]],[[8148,8149],"disallowed"],[[8150,8151],"valid"],[[8152,8152],"mapped",[8144]],[[8153,8153],"mapped",[8145]],[[8154,8154],"mapped",[8054]],[[8155,8155],"mapped",[943]],[[8156,8156],"disallowed"],[[8157,8157],"disallowed_STD3_mapped",[32,788,768]],[[8158,8158],"disallowed_STD3_mapped",[32,788,769]],[[8159,8159],"disallowed_STD3_mapped",[32,788,834]],[[8160,8162],"valid"],[[8163,8163],"mapped",[944]],[[8164,8167],"valid"],[[8168,8168],"mapped",[8160]],[[8169,8169],"mapped",[8161]],[[8170,8170],"mapped",[8058]],[[8171,8171],"mapped",[973]],[[8172,8172],"mapped",[8165]],[[8173,8173],"disallowed_STD3_mapped",[32,776,768]],[[8174,8174],"disallowed_STD3_mapped",[32,776,769]],[[8175,8175],"disallowed_STD3_mapped",[96]],[[8176,8177],"disallowed"],[[8178,8178],"mapped",[8060,953]],[[8179,8179],"mapped",[969,953]],[[8180,8180],"mapped",[974,953]],[[8181,8181],"disallowed"],[[8182,8182],"valid"],[[8183,8183],"mapped",[8182,953]],[[8184,8184],"mapped",[8056]],[[8185,8185],"mapped",[972]],[[8186,8186],"mapped",[8060]],[[8187,8187],"mapped",[974]],[[8188,8188],"mapped",[969,953]],[[8189,8189],"disallowed_STD3_mapped",[32,769]],[[8190,8190],"disallowed_STD3_mapped",[32,788]],[[8191,8191],"disallowed"],[[8192,8202],"disallowed_STD3_mapped",[32]],[[8203,8203],"ignored"],[[8204,8205],"deviation",[]],[[8206,8207],"disallowed"],[[8208,8208],"valid",[],"NV8"],[[8209,8209],"mapped",[8208]],[[8210,8214],"valid",[],"NV8"],[[8215,8215],"disallowed_STD3_mapped",[32,819]],[[8216,8227],"valid",[],"NV8"],[[8228,8230],"disallowed"],[[8231,8231],"valid",[],"NV8"],[[8232,8238],"disallowed"],[[8239,8239],"disallowed_STD3_mapped",[32]],[[8240,8242],"valid",[],"NV8"],[[8243,8243],"mapped",[8242,8242]],[[8244,8244],"mapped",[8242,8242,8242]],[[8245,8245],"valid",[],"NV8"],[[8246,8246],"mapped",[8245,8245]],[[8247,8247],"mapped",[8245,8245,8245]],[[8248,8251],"valid",[],"NV8"],[[8252,8252],"disallowed_STD3_mapped",[33,33]],[[8253,8253],"valid",[],"NV8"],[[8254,8254],"disallowed_STD3_mapped",[32,773]],[[8255,8262],"valid",[],"NV8"],[[8263,8263],"disallowed_STD3_mapped",[63,63]],[[8264,8264],"disallowed_STD3_mapped",[63,33]],[[8265,8265],"disallowed_STD3_mapped",[33,63]],[[8266,8269],"valid",[],"NV8"],[[8270,8274],"valid",[],"NV8"],[[8275,8276],"valid",[],"NV8"],[[8277,8278],"valid",[],"NV8"],[[8279,8279],"mapped",[8242,8242,8242,8242]],[[8280,8286],"valid",[],"NV8"],[[8287,8287],"disallowed_STD3_mapped",[32]],[[8288,8288],"ignored"],[[8289,8291],"disallowed"],[[8292,8292],"ignored"],[[8293,8293],"disallowed"],[[8294,8297],"disallowed"],[[8298,8303],"disallowed"],[[8304,8304],"mapped",[48]],[[8305,8305],"mapped",[105]],[[8306,8307],"disallowed"],[[8308,8308],"mapped",[52]],[[8309,8309],"mapped",[53]],[[8310,8310],"mapped",[54]],[[8311,8311],"mapped",[55]],[[8312,8312],"mapped",[56]],[[8313,8313],"mapped",[57]],[[8314,8314],"disallowed_STD3_mapped",[43]],[[8315,8315],"mapped",[8722]],[[8316,8316],"disallowed_STD3_mapped",[61]],[[8317,8317],"disallowed_STD3_mapped",[40]],[[8318,8318],"disallowed_STD3_mapped",[41]],[[8319,8319],"mapped",[110]],[[8320,8320],"mapped",[48]],[[8321,8321],"mapped",[49]],[[8322,8322],"mapped",[50]],[[8323,8323],"mapped",[51]],[[8324,8324],"mapped",[52]],[[8325,8325],"mapped",[53]],[[8326,8326],"mapped",[54]],[[8327,8327],"mapped",[55]],[[8328,8328],"mapped",[56]],[[8329,8329],"mapped",[57]],[[8330,8330],"disallowed_STD3_mapped",[43]],[[8331,8331],"mapped",[8722]],[[8332,8332],"disallowed_STD3_mapped",[61]],[[8333,8333],"disallowed_STD3_mapped",[40]],[[8334,8334],"disallowed_STD3_mapped",[41]],[[8335,8335],"disallowed"],[[8336,8336],"mapped",[97]],[[8337,8337],"mapped",[101]],[[8338,8338],"mapped",[111]],[[8339,8339],"mapped",[120]],[[8340,8340],"mapped",[601]],[[8341,8341],"mapped",[104]],[[8342,8342],"mapped",[107]],[[8343,8343],"mapped",[108]],[[8344,8344],"mapped",[109]],[[8345,8345],"mapped",[110]],[[8346,8346],"mapped",[112]],[[8347,8347],"mapped",[115]],[[8348,8348],"mapped",[116]],[[8349,8351],"disallowed"],[[8352,8359],"valid",[],"NV8"],[[8360,8360],"mapped",[114,115]],[[8361,8362],"valid",[],"NV8"],[[8363,8363],"valid",[],"NV8"],[[8364,8364],"valid",[],"NV8"],[[8365,8367],"valid",[],"NV8"],[[8368,8369],"valid",[],"NV8"],[[8370,8373],"valid",[],"NV8"],[[8374,8376],"valid",[],"NV8"],[[8377,8377],"valid",[],"NV8"],[[8378,8378],"valid",[],"NV8"],[[8379,8381],"valid",[],"NV8"],[[8382,8382],"valid",[],"NV8"],[[8383,8399],"disallowed"],[[8400,8417],"valid",[],"NV8"],[[8418,8419],"valid",[],"NV8"],[[8420,8426],"valid",[],"NV8"],[[8427,8427],"valid",[],"NV8"],[[8428,8431],"valid",[],"NV8"],[[8432,8432],"valid",[],"NV8"],[[8433,8447],"disallowed"],[[8448,8448],"disallowed_STD3_mapped",[97,47,99]],[[8449,8449],"disallowed_STD3_mapped",[97,47,115]],[[8450,8450],"mapped",[99]],[[8451,8451],"mapped",[176,99]],[[8452,8452],"valid",[],"NV8"],[[8453,8453],"disallowed_STD3_mapped",[99,47,111]],[[8454,8454],"disallowed_STD3_mapped",[99,47,117]],[[8455,8455],"mapped",[603]],[[8456,8456],"valid",[],"NV8"],[[8457,8457],"mapped",[176,102]],[[8458,8458],"mapped",[103]],[[8459,8462],"mapped",[104]],[[8463,8463],"mapped",[295]],[[8464,8465],"mapped",[105]],[[8466,8467],"mapped",[108]],[[8468,8468],"valid",[],"NV8"],[[8469,8469],"mapped",[110]],[[8470,8470],"mapped",[110,111]],[[8471,8472],"valid",[],"NV8"],[[8473,8473],"mapped",[112]],[[8474,8474],"mapped",[113]],[[8475,8477],"mapped",[114]],[[8478,8479],"valid",[],"NV8"],[[8480,8480],"mapped",[115,109]],[[8481,8481],"mapped",[116,101,108]],[[8482,8482],"mapped",[116,109]],[[8483,8483],"valid",[],"NV8"],[[8484,8484],"mapped",[122]],[[8485,8485],"valid",[],"NV8"],[[8486,8486],"mapped",[969]],[[8487,8487],"valid",[],"NV8"],[[8488,8488],"mapped",[122]],[[8489,8489],"valid",[],"NV8"],[[8490,8490],"mapped",[107]],[[8491,8491],"mapped",[229]],[[8492,8492],"mapped",[98]],[[8493,8493],"mapped",[99]],[[8494,8494],"valid",[],"NV8"],[[8495,8496],"mapped",[101]],[[8497,8497],"mapped",[102]],[[8498,8498],"disallowed"],[[8499,8499],"mapped",[109]],[[8500,8500],"mapped",[111]],[[8501,8501],"mapped",[1488]],[[8502,8502],"mapped",[1489]],[[8503,8503],"mapped",[1490]],[[8504,8504],"mapped",[1491]],[[8505,8505],"mapped",[105]],[[8506,8506],"valid",[],"NV8"],[[8507,8507],"mapped",[102,97,120]],[[8508,8508],"mapped",[960]],[[8509,8510],"mapped",[947]],[[8511,8511],"mapped",[960]],[[8512,8512],"mapped",[8721]],[[8513,8516],"valid",[],"NV8"],[[8517,8518],"mapped",[100]],[[8519,8519],"mapped",[101]],[[8520,8520],"mapped",[105]],[[8521,8521],"mapped",[106]],[[8522,8523],"valid",[],"NV8"],[[8524,8524],"valid",[],"NV8"],[[8525,8525],"valid",[],"NV8"],[[8526,8526],"valid"],[[8527,8527],"valid",[],"NV8"],[[8528,8528],"mapped",[49,8260,55]],[[8529,8529],"mapped",[49,8260,57]],[[8530,8530],"mapped",[49,8260,49,48]],[[8531,8531],"mapped",[49,8260,51]],[[8532,8532],"mapped",[50,8260,51]],[[8533,8533],"mapped",[49,8260,53]],[[8534,8534],"mapped",[50,8260,53]],[[8535,8535],"mapped",[51,8260,53]],[[8536,8536],"mapped",[52,8260,53]],[[8537,8537],"mapped",[49,8260,54]],[[8538,8538],"mapped",[53,8260,54]],[[8539,8539],"mapped",[49,8260,56]],[[8540,8540],"mapped",[51,8260,56]],[[8541,8541],"mapped",[53,8260,56]],[[8542,8542],"mapped",[55,8260,56]],[[8543,8543],"mapped",[49,8260]],[[8544,8544],"mapped",[105]],[[8545,8545],"mapped",[105,105]],[[8546,8546],"mapped",[105,105,105]],[[8547,8547],"mapped",[105,118]],[[8548,8548],"mapped",[118]],[[8549,8549],"mapped",[118,105]],[[8550,8550],"mapped",[118,105,105]],[[8551,8551],"mapped",[118,105,105,105]],[[8552,8552],"mapped",[105,120]],[[8553,8553],"mapped",[120]],[[8554,8554],"mapped",[120,105]],[[8555,8555],"mapped",[120,105,105]],[[8556,8556],"mapped",[108]],[[8557,8557],"mapped",[99]],[[8558,8558],"mapped",[100]],[[8559,8559],"mapped",[109]],[[8560,8560],"mapped",[105]],[[8561,8561],"mapped",[105,105]],[[8562,8562],"mapped",[105,105,105]],[[8563,8563],"mapped",[105,118]],[[8564,8564],"mapped",[118]],[[8565,8565],"mapped",[118,105]],[[8566,8566],"mapped",[118,105,105]],[[8567,8567],"mapped",[118,105,105,105]],[[8568,8568],"mapped",[105,120]],[[8569,8569],"mapped",[120]],[[8570,8570],"mapped",[120,105]],[[8571,8571],"mapped",[120,105,105]],[[8572,8572],"mapped",[108]],[[8573,8573],"mapped",[99]],[[8574,8574],"mapped",[100]],[[8575,8575],"mapped",[109]],[[8576,8578],"valid",[],"NV8"],[[8579,8579],"disallowed"],[[8580,8580],"valid"],[[8581,8584],"valid",[],"NV8"],[[8585,8585],"mapped",[48,8260,51]],[[8586,8587],"valid",[],"NV8"],[[8588,8591],"disallowed"],[[8592,8682],"valid",[],"NV8"],[[8683,8691],"valid",[],"NV8"],[[8692,8703],"valid",[],"NV8"],[[8704,8747],"valid",[],"NV8"],[[8748,8748],"mapped",[8747,8747]],[[8749,8749],"mapped",[8747,8747,8747]],[[8750,8750],"valid",[],"NV8"],[[8751,8751],"mapped",[8750,8750]],[[8752,8752],"mapped",[8750,8750,8750]],[[8753,8799],"valid",[],"NV8"],[[8800,8800],"disallowed_STD3_valid"],[[8801,8813],"valid",[],"NV8"],[[8814,8815],"disallowed_STD3_valid"],[[8816,8945],"valid",[],"NV8"],[[8946,8959],"valid",[],"NV8"],[[8960,8960],"valid",[],"NV8"],[[8961,8961],"valid",[],"NV8"],[[8962,9000],"valid",[],"NV8"],[[9001,9001],"mapped",[12296]],[[9002,9002],"mapped",[12297]],[[9003,9082],"valid",[],"NV8"],[[9083,9083],"valid",[],"NV8"],[[9084,9084],"valid",[],"NV8"],[[9085,9114],"valid",[],"NV8"],[[9115,9166],"valid",[],"NV8"],[[9167,9168],"valid",[],"NV8"],[[9169,9179],"valid",[],"NV8"],[[9180,9191],"valid",[],"NV8"],[[9192,9192],"valid",[],"NV8"],[[9193,9203],"valid",[],"NV8"],[[9204,9210],"valid",[],"NV8"],[[9211,9215],"disallowed"],[[9216,9252],"valid",[],"NV8"],[[9253,9254],"valid",[],"NV8"],[[9255,9279],"disallowed"],[[9280,9290],"valid",[],"NV8"],[[9291,9311],"disallowed"],[[9312,9312],"mapped",[49]],[[9313,9313],"mapped",[50]],[[9314,9314],"mapped",[51]],[[9315,9315],"mapped",[52]],[[9316,9316],"mapped",[53]],[[9317,9317],"mapped",[54]],[[9318,9318],"mapped",[55]],[[9319,9319],"mapped",[56]],[[9320,9320],"mapped",[57]],[[9321,9321],"mapped",[49,48]],[[9322,9322],"mapped",[49,49]],[[9323,9323],"mapped",[49,50]],[[9324,9324],"mapped",[49,51]],[[9325,9325],"mapped",[49,52]],[[9326,9326],"mapped",[49,53]],[[9327,9327],"mapped",[49,54]],[[9328,9328],"mapped",[49,55]],[[9329,9329],"mapped",[49,56]],[[9330,9330],"mapped",[49,57]],[[9331,9331],"mapped",[50,48]],[[9332,9332],"disallowed_STD3_mapped",[40,49,41]],[[9333,9333],"disallowed_STD3_mapped",[40,50,41]],[[9334,9334],"disallowed_STD3_mapped",[40,51,41]],[[9335,9335],"disallowed_STD3_mapped",[40,52,41]],[[9336,9336],"disallowed_STD3_mapped",[40,53,41]],[[9337,9337],"disallowed_STD3_mapped",[40,54,41]],[[9338,9338],"disallowed_STD3_mapped",[40,55,41]],[[9339,9339],"disallowed_STD3_mapped",[40,56,41]],[[9340,9340],"disallowed_STD3_mapped",[40,57,41]],[[9341,9341],"disallowed_STD3_mapped",[40,49,48,41]],[[9342,9342],"disallowed_STD3_mapped",[40,49,49,41]],[[9343,9343],"disallowed_STD3_mapped",[40,49,50,41]],[[9344,9344],"disallowed_STD3_mapped",[40,49,51,41]],[[9345,9345],"disallowed_STD3_mapped",[40,49,52,41]],[[9346,9346],"disallowed_STD3_mapped",[40,49,53,41]],[[9347,9347],"disallowed_STD3_mapped",[40,49,54,41]],[[9348,9348],"disallowed_STD3_mapped",[40,49,55,41]],[[9349,9349],"disallowed_STD3_mapped",[40,49,56,41]],[[9350,9350],"disallowed_STD3_mapped",[40,49,57,41]],[[9351,9351],"disallowed_STD3_mapped",[40,50,48,41]],[[9352,9371],"disallowed"],[[9372,9372],"disallowed_STD3_mapped",[40,97,41]],[[9373,9373],"disallowed_STD3_mapped",[40,98,41]],[[9374,9374],"disallowed_STD3_mapped",[40,99,41]],[[9375,9375],"disallowed_STD3_mapped",[40,100,41]],[[9376,9376],"disallowed_STD3_mapped",[40,101,41]],[[9377,9377],"disallowed_STD3_mapped",[40,102,41]],[[9378,9378],"disallowed_STD3_mapped",[40,103,41]],[[9379,9379],"disallowed_STD3_mapped",[40,104,41]],[[9380,9380],"disallowed_STD3_mapped",[40,105,41]],[[9381,9381],"disallowed_STD3_mapped",[40,106,41]],[[9382,9382],"disallowed_STD3_mapped",[40,107,41]],[[9383,9383],"disallowed_STD3_mapped",[40,108,41]],[[9384,9384],"disallowed_STD3_mapped",[40,109,41]],[[9385,9385],"disallowed_STD3_mapped",[40,110,41]],[[9386,9386],"disallowed_STD3_mapped",[40,111,41]],[[9387,9387],"disallowed_STD3_mapped",[40,112,41]],[[9388,9388],"disallowed_STD3_mapped",[40,113,41]],[[9389,9389],"disallowed_STD3_mapped",[40,114,41]],[[9390,9390],"disallowed_STD3_mapped",[40,115,41]],[[9391,9391],"disallowed_STD3_mapped",[40,116,41]],[[9392,9392],"disallowed_STD3_mapped",[40,117,41]],[[9393,9393],"disallowed_STD3_mapped",[40,118,41]],[[9394,9394],"disallowed_STD3_mapped",[40,119,41]],[[9395,9395],"disallowed_STD3_mapped",[40,120,41]],[[9396,9396],"disallowed_STD3_mapped",[40,121,41]],[[9397,9397],"disallowed_STD3_mapped",[40,122,41]],[[9398,9398],"mapped",[97]],[[9399,9399],"mapped",[98]],[[9400,9400],"mapped",[99]],[[9401,9401],"mapped",[100]],[[9402,9402],"mapped",[101]],[[9403,9403],"mapped",[102]],[[9404,9404],"mapped",[103]],[[9405,9405],"mapped",[104]],[[9406,9406],"mapped",[105]],[[9407,9407],"mapped",[106]],[[9408,9408],"mapped",[107]],[[9409,9409],"mapped",[108]],[[9410,9410],"mapped",[109]],[[9411,9411],"mapped",[110]],[[9412,9412],"mapped",[111]],[[9413,9413],"mapped",[112]],[[9414,9414],"mapped",[113]],[[9415,9415],"mapped",[114]],[[9416,9416],"mapped",[115]],[[9417,9417],"mapped",[116]],[[9418,9418],"mapped",[117]],[[9419,9419],"mapped",[118]],[[9420,9420],"mapped",[119]],[[9421,9421],"mapped",[120]],[[9422,9422],"mapped",[121]],[[9423,9423],"mapped",[122]],[[9424,9424],"mapped",[97]],[[9425,9425],"mapped",[98]],[[9426,9426],"mapped",[99]],[[9427,9427],"mapped",[100]],[[9428,9428],"mapped",[101]],[[9429,9429],"mapped",[102]],[[9430,9430],"mapped",[103]],[[9431,9431],"mapped",[104]],[[9432,9432],"mapped",[105]],[[9433,9433],"mapped",[106]],[[9434,9434],"mapped",[107]],[[9435,9435],"mapped",[108]],[[9436,9436],"mapped",[109]],[[9437,9437],"mapped",[110]],[[9438,9438],"mapped",[111]],[[9439,9439],"mapped",[112]],[[9440,9440],"mapped",[113]],[[9441,9441],"mapped",[114]],[[9442,9442],"mapped",[115]],[[9443,9443],"mapped",[116]],[[9444,9444],"mapped",[117]],[[9445,9445],"mapped",[118]],[[9446,9446],"mapped",[119]],[[9447,9447],"mapped",[120]],[[9448,9448],"mapped",[121]],[[9449,9449],"mapped",[122]],[[9450,9450],"mapped",[48]],[[9451,9470],"valid",[],"NV8"],[[9471,9471],"valid",[],"NV8"],[[9472,9621],"valid",[],"NV8"],[[9622,9631],"valid",[],"NV8"],[[9632,9711],"valid",[],"NV8"],[[9712,9719],"valid",[],"NV8"],[[9720,9727],"valid",[],"NV8"],[[9728,9747],"valid",[],"NV8"],[[9748,9749],"valid",[],"NV8"],[[9750,9751],"valid",[],"NV8"],[[9752,9752],"valid",[],"NV8"],[[9753,9753],"valid",[],"NV8"],[[9754,9839],"valid",[],"NV8"],[[9840,9841],"valid",[],"NV8"],[[9842,9853],"valid",[],"NV8"],[[9854,9855],"valid",[],"NV8"],[[9856,9865],"valid",[],"NV8"],[[9866,9873],"valid",[],"NV8"],[[9874,9884],"valid",[],"NV8"],[[9885,9885],"valid",[],"NV8"],[[9886,9887],"valid",[],"NV8"],[[9888,9889],"valid",[],"NV8"],[[9890,9905],"valid",[],"NV8"],[[9906,9906],"valid",[],"NV8"],[[9907,9916],"valid",[],"NV8"],[[9917,9919],"valid",[],"NV8"],[[9920,9923],"valid",[],"NV8"],[[9924,9933],"valid",[],"NV8"],[[9934,9934],"valid",[],"NV8"],[[9935,9953],"valid",[],"NV8"],[[9954,9954],"valid",[],"NV8"],[[9955,9955],"valid",[],"NV8"],[[9956,9959],"valid",[],"NV8"],[[9960,9983],"valid",[],"NV8"],[[9984,9984],"valid",[],"NV8"],[[9985,9988],"valid",[],"NV8"],[[9989,9989],"valid",[],"NV8"],[[9990,9993],"valid",[],"NV8"],[[9994,9995],"valid",[],"NV8"],[[9996,10023],"valid",[],"NV8"],[[10024,10024],"valid",[],"NV8"],[[10025,10059],"valid",[],"NV8"],[[10060,10060],"valid",[],"NV8"],[[10061,10061],"valid",[],"NV8"],[[10062,10062],"valid",[],"NV8"],[[10063,10066],"valid",[],"NV8"],[[10067,10069],"valid",[],"NV8"],[[10070,10070],"valid",[],"NV8"],[[10071,10071],"valid",[],"NV8"],[[10072,10078],"valid",[],"NV8"],[[10079,10080],"valid",[],"NV8"],[[10081,10087],"valid",[],"NV8"],[[10088,10101],"valid",[],"NV8"],[[10102,10132],"valid",[],"NV8"],[[10133,10135],"valid",[],"NV8"],[[10136,10159],"valid",[],"NV8"],[[10160,10160],"valid",[],"NV8"],[[10161,10174],"valid",[],"NV8"],[[10175,10175],"valid",[],"NV8"],[[10176,10182],"valid",[],"NV8"],[[10183,10186],"valid",[],"NV8"],[[10187,10187],"valid",[],"NV8"],[[10188,10188],"valid",[],"NV8"],[[10189,10189],"valid",[],"NV8"],[[10190,10191],"valid",[],"NV8"],[[10192,10219],"valid",[],"NV8"],[[10220,10223],"valid",[],"NV8"],[[10224,10239],"valid",[],"NV8"],[[10240,10495],"valid",[],"NV8"],[[10496,10763],"valid",[],"NV8"],[[10764,10764],"mapped",[8747,8747,8747,8747]],[[10765,10867],"valid",[],"NV8"],[[10868,10868],"disallowed_STD3_mapped",[58,58,61]],[[10869,10869],"disallowed_STD3_mapped",[61,61]],[[10870,10870],"disallowed_STD3_mapped",[61,61,61]],[[10871,10971],"valid",[],"NV8"],[[10972,10972],"mapped",[10973,824]],[[10973,11007],"valid",[],"NV8"],[[11008,11021],"valid",[],"NV8"],[[11022,11027],"valid",[],"NV8"],[[11028,11034],"valid",[],"NV8"],[[11035,11039],"valid",[],"NV8"],[[11040,11043],"valid",[],"NV8"],[[11044,11084],"valid",[],"NV8"],[[11085,11087],"valid",[],"NV8"],[[11088,11092],"valid",[],"NV8"],[[11093,11097],"valid",[],"NV8"],[[11098,11123],"valid",[],"NV8"],[[11124,11125],"disallowed"],[[11126,11157],"valid",[],"NV8"],[[11158,11159],"disallowed"],[[11160,11193],"valid",[],"NV8"],[[11194,11196],"disallowed"],[[11197,11208],"valid",[],"NV8"],[[11209,11209],"disallowed"],[[11210,11217],"valid",[],"NV8"],[[11218,11243],"disallowed"],[[11244,11247],"valid",[],"NV8"],[[11248,11263],"disallowed"],[[11264,11264],"mapped",[11312]],[[11265,11265],"mapped",[11313]],[[11266,11266],"mapped",[11314]],[[11267,11267],"mapped",[11315]],[[11268,11268],"mapped",[11316]],[[11269,11269],"mapped",[11317]],[[11270,11270],"mapped",[11318]],[[11271,11271],"mapped",[11319]],[[11272,11272],"mapped",[11320]],[[11273,11273],"mapped",[11321]],[[11274,11274],"mapped",[11322]],[[11275,11275],"mapped",[11323]],[[11276,11276],"mapped",[11324]],[[11277,11277],"mapped",[11325]],[[11278,11278],"mapped",[11326]],[[11279,11279],"mapped",[11327]],[[11280,11280],"mapped",[11328]],[[11281,11281],"mapped",[11329]],[[11282,11282],"mapped",[11330]],[[11283,11283],"mapped",[11331]],[[11284,11284],"mapped",[11332]],[[11285,11285],"mapped",[11333]],[[11286,11286],"mapped",[11334]],[[11287,11287],"mapped",[11335]],[[11288,11288],"mapped",[11336]],[[11289,11289],"mapped",[11337]],[[11290,11290],"mapped",[11338]],[[11291,11291],"mapped",[11339]],[[11292,11292],"mapped",[11340]],[[11293,11293],"mapped",[11341]],[[11294,11294],"mapped",[11342]],[[11295,11295],"mapped",[11343]],[[11296,11296],"mapped",[11344]],[[11297,11297],"mapped",[11345]],[[11298,11298],"mapped",[11346]],[[11299,11299],"mapped",[11347]],[[11300,11300],"mapped",[11348]],[[11301,11301],"mapped",[11349]],[[11302,11302],"mapped",[11350]],[[11303,11303],"mapped",[11351]],[[11304,11304],"mapped",[11352]],[[11305,11305],"mapped",[11353]],[[11306,11306],"mapped",[11354]],[[11307,11307],"mapped",[11355]],[[11308,11308],"mapped",[11356]],[[11309,11309],"mapped",[11357]],[[11310,11310],"mapped",[11358]],[[11311,11311],"disallowed"],[[11312,11358],"valid"],[[11359,11359],"disallowed"],[[11360,11360],"mapped",[11361]],[[11361,11361],"valid"],[[11362,11362],"mapped",[619]],[[11363,11363],"mapped",[7549]],[[11364,11364],"mapped",[637]],[[11365,11366],"valid"],[[11367,11367],"mapped",[11368]],[[11368,11368],"valid"],[[11369,11369],"mapped",[11370]],[[11370,11370],"valid"],[[11371,11371],"mapped",[11372]],[[11372,11372],"valid"],[[11373,11373],"mapped",[593]],[[11374,11374],"mapped",[625]],[[11375,11375],"mapped",[592]],[[11376,11376],"mapped",[594]],[[11377,11377],"valid"],[[11378,11378],"mapped",[11379]],[[11379,11379],"valid"],[[11380,11380],"valid"],[[11381,11381],"mapped",[11382]],[[11382,11383],"valid"],[[11384,11387],"valid"],[[11388,11388],"mapped",[106]],[[11389,11389],"mapped",[118]],[[11390,11390],"mapped",[575]],[[11391,11391],"mapped",[576]],[[11392,11392],"mapped",[11393]],[[11393,11393],"valid"],[[11394,11394],"mapped",[11395]],[[11395,11395],"valid"],[[11396,11396],"mapped",[11397]],[[11397,11397],"valid"],[[11398,11398],"mapped",[11399]],[[11399,11399],"valid"],[[11400,11400],"mapped",[11401]],[[11401,11401],"valid"],[[11402,11402],"mapped",[11403]],[[11403,11403],"valid"],[[11404,11404],"mapped",[11405]],[[11405,11405],"valid"],[[11406,11406],"mapped",[11407]],[[11407,11407],"valid"],[[11408,11408],"mapped",[11409]],[[11409,11409],"valid"],[[11410,11410],"mapped",[11411]],[[11411,11411],"valid"],[[11412,11412],"mapped",[11413]],[[11413,11413],"valid"],[[11414,11414],"mapped",[11415]],[[11415,11415],"valid"],[[11416,11416],"mapped",[11417]],[[11417,11417],"valid"],[[11418,11418],"mapped",[11419]],[[11419,11419],"valid"],[[11420,11420],"mapped",[11421]],[[11421,11421],"valid"],[[11422,11422],"mapped",[11423]],[[11423,11423],"valid"],[[11424,11424],"mapped",[11425]],[[11425,11425],"valid"],[[11426,11426],"mapped",[11427]],[[11427,11427],"valid"],[[11428,11428],"mapped",[11429]],[[11429,11429],"valid"],[[11430,11430],"mapped",[11431]],[[11431,11431],"valid"],[[11432,11432],"mapped",[11433]],[[11433,11433],"valid"],[[11434,11434],"mapped",[11435]],[[11435,11435],"valid"],[[11436,11436],"mapped",[11437]],[[11437,11437],"valid"],[[11438,11438],"mapped",[11439]],[[11439,11439],"valid"],[[11440,11440],"mapped",[11441]],[[11441,11441],"valid"],[[11442,11442],"mapped",[11443]],[[11443,11443],"valid"],[[11444,11444],"mapped",[11445]],[[11445,11445],"valid"],[[11446,11446],"mapped",[11447]],[[11447,11447],"valid"],[[11448,11448],"mapped",[11449]],[[11449,11449],"valid"],[[11450,11450],"mapped",[11451]],[[11451,11451],"valid"],[[11452,11452],"mapped",[11453]],[[11453,11453],"valid"],[[11454,11454],"mapped",[11455]],[[11455,11455],"valid"],[[11456,11456],"mapped",[11457]],[[11457,11457],"valid"],[[11458,11458],"mapped",[11459]],[[11459,11459],"valid"],[[11460,11460],"mapped",[11461]],[[11461,11461],"valid"],[[11462,11462],"mapped",[11463]],[[11463,11463],"valid"],[[11464,11464],"mapped",[11465]],[[11465,11465],"valid"],[[11466,11466],"mapped",[11467]],[[11467,11467],"valid"],[[11468,11468],"mapped",[11469]],[[11469,11469],"valid"],[[11470,11470],"mapped",[11471]],[[11471,11471],"valid"],[[11472,11472],"mapped",[11473]],[[11473,11473],"valid"],[[11474,11474],"mapped",[11475]],[[11475,11475],"valid"],[[11476,11476],"mapped",[11477]],[[11477,11477],"valid"],[[11478,11478],"mapped",[11479]],[[11479,11479],"valid"],[[11480,11480],"mapped",[11481]],[[11481,11481],"valid"],[[11482,11482],"mapped",[11483]],[[11483,11483],"valid"],[[11484,11484],"mapped",[11485]],[[11485,11485],"valid"],[[11486,11486],"mapped",[11487]],[[11487,11487],"valid"],[[11488,11488],"mapped",[11489]],[[11489,11489],"valid"],[[11490,11490],"mapped",[11491]],[[11491,11492],"valid"],[[11493,11498],"valid",[],"NV8"],[[11499,11499],"mapped",[11500]],[[11500,11500],"valid"],[[11501,11501],"mapped",[11502]],[[11502,11505],"valid"],[[11506,11506],"mapped",[11507]],[[11507,11507],"valid"],[[11508,11512],"disallowed"],[[11513,11519],"valid",[],"NV8"],[[11520,11557],"valid"],[[11558,11558],"disallowed"],[[11559,11559],"valid"],[[11560,11564],"disallowed"],[[11565,11565],"valid"],[[11566,11567],"disallowed"],[[11568,11621],"valid"],[[11622,11623],"valid"],[[11624,11630],"disallowed"],[[11631,11631],"mapped",[11617]],[[11632,11632],"valid",[],"NV8"],[[11633,11646],"disallowed"],[[11647,11647],"valid"],[[11648,11670],"valid"],[[11671,11679],"disallowed"],[[11680,11686],"valid"],[[11687,11687],"disallowed"],[[11688,11694],"valid"],[[11695,11695],"disallowed"],[[11696,11702],"valid"],[[11703,11703],"disallowed"],[[11704,11710],"valid"],[[11711,11711],"disallowed"],[[11712,11718],"valid"],[[11719,11719],"disallowed"],[[11720,11726],"valid"],[[11727,11727],"disallowed"],[[11728,11734],"valid"],[[11735,11735],"disallowed"],[[11736,11742],"valid"],[[11743,11743],"disallowed"],[[11744,11775],"valid"],[[11776,11799],"valid",[],"NV8"],[[11800,11803],"valid",[],"NV8"],[[11804,11805],"valid",[],"NV8"],[[11806,11822],"valid",[],"NV8"],[[11823,11823],"valid"],[[11824,11824],"valid",[],"NV8"],[[11825,11825],"valid",[],"NV8"],[[11826,11835],"valid",[],"NV8"],[[11836,11842],"valid",[],"NV8"],[[11843,11903],"disallowed"],[[11904,11929],"valid",[],"NV8"],[[11930,11930],"disallowed"],[[11931,11934],"valid",[],"NV8"],[[11935,11935],"mapped",[27597]],[[11936,12018],"valid",[],"NV8"],[[12019,12019],"mapped",[40863]],[[12020,12031],"disallowed"],[[12032,12032],"mapped",[19968]],[[12033,12033],"mapped",[20008]],[[12034,12034],"mapped",[20022]],[[12035,12035],"mapped",[20031]],[[12036,12036],"mapped",[20057]],[[12037,12037],"mapped",[20101]],[[12038,12038],"mapped",[20108]],[[12039,12039],"mapped",[20128]],[[12040,12040],"mapped",[20154]],[[12041,12041],"mapped",[20799]],[[12042,12042],"mapped",[20837]],[[12043,12043],"mapped",[20843]],[[12044,12044],"mapped",[20866]],[[12045,12045],"mapped",[20886]],[[12046,12046],"mapped",[20907]],[[12047,12047],"mapped",[20960]],[[12048,12048],"mapped",[20981]],[[12049,12049],"mapped",[20992]],[[12050,12050],"mapped",[21147]],[[12051,12051],"mapped",[21241]],[[12052,12052],"mapped",[21269]],[[12053,12053],"mapped",[21274]],[[12054,12054],"mapped",[21304]],[[12055,12055],"mapped",[21313]],[[12056,12056],"mapped",[21340]],[[12057,12057],"mapped",[21353]],[[12058,12058],"mapped",[21378]],[[12059,12059],"mapped",[21430]],[[12060,12060],"mapped",[21448]],[[12061,12061],"mapped",[21475]],[[12062,12062],"mapped",[22231]],[[12063,12063],"mapped",[22303]],[[12064,12064],"mapped",[22763]],[[12065,12065],"mapped",[22786]],[[12066,12066],"mapped",[22794]],[[12067,12067],"mapped",[22805]],[[12068,12068],"mapped",[22823]],[[12069,12069],"mapped",[22899]],[[12070,12070],"mapped",[23376]],[[12071,12071],"mapped",[23424]],[[12072,12072],"mapped",[23544]],[[12073,12073],"mapped",[23567]],[[12074,12074],"mapped",[23586]],[[12075,12075],"mapped",[23608]],[[12076,12076],"mapped",[23662]],[[12077,12077],"mapped",[23665]],[[12078,12078],"mapped",[24027]],[[12079,12079],"mapped",[24037]],[[12080,12080],"mapped",[24049]],[[12081,12081],"mapped",[24062]],[[12082,12082],"mapped",[24178]],[[12083,12083],"mapped",[24186]],[[12084,12084],"mapped",[24191]],[[12085,12085],"mapped",[24308]],[[12086,12086],"mapped",[24318]],[[12087,12087],"mapped",[24331]],[[12088,12088],"mapped",[24339]],[[12089,12089],"mapped",[24400]],[[12090,12090],"mapped",[24417]],[[12091,12091],"mapped",[24435]],[[12092,12092],"mapped",[24515]],[[12093,12093],"mapped",[25096]],[[12094,12094],"mapped",[25142]],[[12095,12095],"mapped",[25163]],[[12096,12096],"mapped",[25903]],[[12097,12097],"mapped",[25908]],[[12098,12098],"mapped",[25991]],[[12099,12099],"mapped",[26007]],[[12100,12100],"mapped",[26020]],[[12101,12101],"mapped",[26041]],[[12102,12102],"mapped",[26080]],[[12103,12103],"mapped",[26085]],[[12104,12104],"mapped",[26352]],[[12105,12105],"mapped",[26376]],[[12106,12106],"mapped",[26408]],[[12107,12107],"mapped",[27424]],[[12108,12108],"mapped",[27490]],[[12109,12109],"mapped",[27513]],[[12110,12110],"mapped",[27571]],[[12111,12111],"mapped",[27595]],[[12112,12112],"mapped",[27604]],[[12113,12113],"mapped",[27611]],[[12114,12114],"mapped",[27663]],[[12115,12115],"mapped",[27668]],[[12116,12116],"mapped",[27700]],[[12117,12117],"mapped",[28779]],[[12118,12118],"mapped",[29226]],[[12119,12119],"mapped",[29238]],[[12120,12120],"mapped",[29243]],[[12121,12121],"mapped",[29247]],[[12122,12122],"mapped",[29255]],[[12123,12123],"mapped",[29273]],[[12124,12124],"mapped",[29275]],[[12125,12125],"mapped",[29356]],[[12126,12126],"mapped",[29572]],[[12127,12127],"mapped",[29577]],[[12128,12128],"mapped",[29916]],[[12129,12129],"mapped",[29926]],[[12130,12130],"mapped",[29976]],[[12131,12131],"mapped",[29983]],[[12132,12132],"mapped",[29992]],[[12133,12133],"mapped",[30000]],[[12134,12134],"mapped",[30091]],[[12135,12135],"mapped",[30098]],[[12136,12136],"mapped",[30326]],[[12137,12137],"mapped",[30333]],[[12138,12138],"mapped",[30382]],[[12139,12139],"mapped",[30399]],[[12140,12140],"mapped",[30446]],[[12141,12141],"mapped",[30683]],[[12142,12142],"mapped",[30690]],[[12143,12143],"mapped",[30707]],[[12144,12144],"mapped",[31034]],[[12145,12145],"mapped",[31160]],[[12146,12146],"mapped",[31166]],[[12147,12147],"mapped",[31348]],[[12148,12148],"mapped",[31435]],[[12149,12149],"mapped",[31481]],[[12150,12150],"mapped",[31859]],[[12151,12151],"mapped",[31992]],[[12152,12152],"mapped",[32566]],[[12153,12153],"mapped",[32593]],[[12154,12154],"mapped",[32650]],[[12155,12155],"mapped",[32701]],[[12156,12156],"mapped",[32769]],[[12157,12157],"mapped",[32780]],[[12158,12158],"mapped",[32786]],[[12159,12159],"mapped",[32819]],[[12160,12160],"mapped",[32895]],[[12161,12161],"mapped",[32905]],[[12162,12162],"mapped",[33251]],[[12163,12163],"mapped",[33258]],[[12164,12164],"mapped",[33267]],[[12165,12165],"mapped",[33276]],[[12166,12166],"mapped",[33292]],[[12167,12167],"mapped",[33307]],[[12168,12168],"mapped",[33311]],[[12169,12169],"mapped",[33390]],[[12170,12170],"mapped",[33394]],[[12171,12171],"mapped",[33400]],[[12172,12172],"mapped",[34381]],[[12173,12173],"mapped",[34411]],[[12174,12174],"mapped",[34880]],[[12175,12175],"mapped",[34892]],[[12176,12176],"mapped",[34915]],[[12177,12177],"mapped",[35198]],[[12178,12178],"mapped",[35211]],[[12179,12179],"mapped",[35282]],[[12180,12180],"mapped",[35328]],[[12181,12181],"mapped",[35895]],[[12182,12182],"mapped",[35910]],[[12183,12183],"mapped",[35925]],[[12184,12184],"mapped",[35960]],[[12185,12185],"mapped",[35997]],[[12186,12186],"mapped",[36196]],[[12187,12187],"mapped",[36208]],[[12188,12188],"mapped",[36275]],[[12189,12189],"mapped",[36523]],[[12190,12190],"mapped",[36554]],[[12191,12191],"mapped",[36763]],[[12192,12192],"mapped",[36784]],[[12193,12193],"mapped",[36789]],[[12194,12194],"mapped",[37009]],[[12195,12195],"mapped",[37193]],[[12196,12196],"mapped",[37318]],[[12197,12197],"mapped",[37324]],[[12198,12198],"mapped",[37329]],[[12199,12199],"mapped",[38263]],[[12200,12200],"mapped",[38272]],[[12201,12201],"mapped",[38428]],[[12202,12202],"mapped",[38582]],[[12203,12203],"mapped",[38585]],[[12204,12204],"mapped",[38632]],[[12205,12205],"mapped",[38737]],[[12206,12206],"mapped",[38750]],[[12207,12207],"mapped",[38754]],[[12208,12208],"mapped",[38761]],[[12209,12209],"mapped",[38859]],[[12210,12210],"mapped",[38893]],[[12211,12211],"mapped",[38899]],[[12212,12212],"mapped",[38913]],[[12213,12213],"mapped",[39080]],[[12214,12214],"mapped",[39131]],[[12215,12215],"mapped",[39135]],[[12216,12216],"mapped",[39318]],[[12217,12217],"mapped",[39321]],[[12218,12218],"mapped",[39340]],[[12219,12219],"mapped",[39592]],[[12220,12220],"mapped",[39640]],[[12221,12221],"mapped",[39647]],[[12222,12222],"mapped",[39717]],[[12223,12223],"mapped",[39727]],[[12224,12224],"mapped",[39730]],[[12225,12225],"mapped",[39740]],[[12226,12226],"mapped",[39770]],[[12227,12227],"mapped",[40165]],[[12228,12228],"mapped",[40565]],[[12229,12229],"mapped",[40575]],[[12230,12230],"mapped",[40613]],[[12231,12231],"mapped",[40635]],[[12232,12232],"mapped",[40643]],[[12233,12233],"mapped",[40653]],[[12234,12234],"mapped",[40657]],[[12235,12235],"mapped",[40697]],[[12236,12236],"mapped",[40701]],[[12237,12237],"mapped",[40718]],[[12238,12238],"mapped",[40723]],[[12239,12239],"mapped",[40736]],[[12240,12240],"mapped",[40763]],[[12241,12241],"mapped",[40778]],[[12242,12242],"mapped",[40786]],[[12243,12243],"mapped",[40845]],[[12244,12244],"mapped",[40860]],[[12245,12245],"mapped",[40864]],[[12246,12271],"disallowed"],[[12272,12283],"disallowed"],[[12284,12287],"disallowed"],[[12288,12288],"disallowed_STD3_mapped",[32]],[[12289,12289],"valid",[],"NV8"],[[12290,12290],"mapped",[46]],[[12291,12292],"valid",[],"NV8"],[[12293,12295],"valid"],[[12296,12329],"valid",[],"NV8"],[[12330,12333],"valid"],[[12334,12341],"valid",[],"NV8"],[[12342,12342],"mapped",[12306]],[[12343,12343],"valid",[],"NV8"],[[12344,12344],"mapped",[21313]],[[12345,12345],"mapped",[21316]],[[12346,12346],"mapped",[21317]],[[12347,12347],"valid",[],"NV8"],[[12348,12348],"valid"],[[12349,12349],"valid",[],"NV8"],[[12350,12350],"valid",[],"NV8"],[[12351,12351],"valid",[],"NV8"],[[12352,12352],"disallowed"],[[12353,12436],"valid"],[[12437,12438],"valid"],[[12439,12440],"disallowed"],[[12441,12442],"valid"],[[12443,12443],"disallowed_STD3_mapped",[32,12441]],[[12444,12444],"disallowed_STD3_mapped",[32,12442]],[[12445,12446],"valid"],[[12447,12447],"mapped",[12424,12426]],[[12448,12448],"valid",[],"NV8"],[[12449,12542],"valid"],[[12543,12543],"mapped",[12467,12488]],[[12544,12548],"disallowed"],[[12549,12588],"valid"],[[12589,12589],"valid"],[[12590,12592],"disallowed"],[[12593,12593],"mapped",[4352]],[[12594,12594],"mapped",[4353]],[[12595,12595],"mapped",[4522]],[[12596,12596],"mapped",[4354]],[[12597,12597],"mapped",[4524]],[[12598,12598],"mapped",[4525]],[[12599,12599],"mapped",[4355]],[[12600,12600],"mapped",[4356]],[[12601,12601],"mapped",[4357]],[[12602,12602],"mapped",[4528]],[[12603,12603],"mapped",[4529]],[[12604,12604],"mapped",[4530]],[[12605,12605],"mapped",[4531]],[[12606,12606],"mapped",[4532]],[[12607,12607],"mapped",[4533]],[[12608,12608],"mapped",[4378]],[[12609,12609],"mapped",[4358]],[[12610,12610],"mapped",[4359]],[[12611,12611],"mapped",[4360]],[[12612,12612],"mapped",[4385]],[[12613,12613],"mapped",[4361]],[[12614,12614],"mapped",[4362]],[[12615,12615],"mapped",[4363]],[[12616,12616],"mapped",[4364]],[[12617,12617],"mapped",[4365]],[[12618,12618],"mapped",[4366]],[[12619,12619],"mapped",[4367]],[[12620,12620],"mapped",[4368]],[[12621,12621],"mapped",[4369]],[[12622,12622],"mapped",[4370]],[[12623,12623],"mapped",[4449]],[[12624,12624],"mapped",[4450]],[[12625,12625],"mapped",[4451]],[[12626,12626],"mapped",[4452]],[[12627,12627],"mapped",[4453]],[[12628,12628],"mapped",[4454]],[[12629,12629],"mapped",[4455]],[[12630,12630],"mapped",[4456]],[[12631,12631],"mapped",[4457]],[[12632,12632],"mapped",[4458]],[[12633,12633],"mapped",[4459]],[[12634,12634],"mapped",[4460]],[[12635,12635],"mapped",[4461]],[[12636,12636],"mapped",[4462]],[[12637,12637],"mapped",[4463]],[[12638,12638],"mapped",[4464]],[[12639,12639],"mapped",[4465]],[[12640,12640],"mapped",[4466]],[[12641,12641],"mapped",[4467]],[[12642,12642],"mapped",[4468]],[[12643,12643],"mapped",[4469]],[[12644,12644],"disallowed"],[[12645,12645],"mapped",[4372]],[[12646,12646],"mapped",[4373]],[[12647,12647],"mapped",[4551]],[[12648,12648],"mapped",[4552]],[[12649,12649],"mapped",[4556]],[[12650,12650],"mapped",[4558]],[[12651,12651],"mapped",[4563]],[[12652,12652],"mapped",[4567]],[[12653,12653],"mapped",[4569]],[[12654,12654],"mapped",[4380]],[[12655,12655],"mapped",[4573]],[[12656,12656],"mapped",[4575]],[[12657,12657],"mapped",[4381]],[[12658,12658],"mapped",[4382]],[[12659,12659],"mapped",[4384]],[[12660,12660],"mapped",[4386]],[[12661,12661],"mapped",[4387]],[[12662,12662],"mapped",[4391]],[[12663,12663],"mapped",[4393]],[[12664,12664],"mapped",[4395]],[[12665,12665],"mapped",[4396]],[[12666,12666],"mapped",[4397]],[[12667,12667],"mapped",[4398]],[[12668,12668],"mapped",[4399]],[[12669,12669],"mapped",[4402]],[[12670,12670],"mapped",[4406]],[[12671,12671],"mapped",[4416]],[[12672,12672],"mapped",[4423]],[[12673,12673],"mapped",[4428]],[[12674,12674],"mapped",[4593]],[[12675,12675],"mapped",[4594]],[[12676,12676],"mapped",[4439]],[[12677,12677],"mapped",[4440]],[[12678,12678],"mapped",[4441]],[[12679,12679],"mapped",[4484]],[[12680,12680],"mapped",[4485]],[[12681,12681],"mapped",[4488]],[[12682,12682],"mapped",[4497]],[[12683,12683],"mapped",[4498]],[[12684,12684],"mapped",[4500]],[[12685,12685],"mapped",[4510]],[[12686,12686],"mapped",[4513]],[[12687,12687],"disallowed"],[[12688,12689],"valid",[],"NV8"],[[12690,12690],"mapped",[19968]],[[12691,12691],"mapped",[20108]],[[12692,12692],"mapped",[19977]],[[12693,12693],"mapped",[22235]],[[12694,12694],"mapped",[19978]],[[12695,12695],"mapped",[20013]],[[12696,12696],"mapped",[19979]],[[12697,12697],"mapped",[30002]],[[12698,12698],"mapped",[20057]],[[12699,12699],"mapped",[19993]],[[12700,12700],"mapped",[19969]],[[12701,12701],"mapped",[22825]],[[12702,12702],"mapped",[22320]],[[12703,12703],"mapped",[20154]],[[12704,12727],"valid"],[[12728,12730],"valid"],[[12731,12735],"disallowed"],[[12736,12751],"valid",[],"NV8"],[[12752,12771],"valid",[],"NV8"],[[12772,12783],"disallowed"],[[12784,12799],"valid"],[[12800,12800],"disallowed_STD3_mapped",[40,4352,41]],[[12801,12801],"disallowed_STD3_mapped",[40,4354,41]],[[12802,12802],"disallowed_STD3_mapped",[40,4355,41]],[[12803,12803],"disallowed_STD3_mapped",[40,4357,41]],[[12804,12804],"disallowed_STD3_mapped",[40,4358,41]],[[12805,12805],"disallowed_STD3_mapped",[40,4359,41]],[[12806,12806],"disallowed_STD3_mapped",[40,4361,41]],[[12807,12807],"disallowed_STD3_mapped",[40,4363,41]],[[12808,12808],"disallowed_STD3_mapped",[40,4364,41]],[[12809,12809],"disallowed_STD3_mapped",[40,4366,41]],[[12810,12810],"disallowed_STD3_mapped",[40,4367,41]],[[12811,12811],"disallowed_STD3_mapped",[40,4368,41]],[[12812,12812],"disallowed_STD3_mapped",[40,4369,41]],[[12813,12813],"disallowed_STD3_mapped",[40,4370,41]],[[12814,12814],"disallowed_STD3_mapped",[40,44032,41]],[[12815,12815],"disallowed_STD3_mapped",[40,45208,41]],[[12816,12816],"disallowed_STD3_mapped",[40,45796,41]],[[12817,12817],"disallowed_STD3_mapped",[40,46972,41]],[[12818,12818],"disallowed_STD3_mapped",[40,47560,41]],[[12819,12819],"disallowed_STD3_mapped",[40,48148,41]],[[12820,12820],"disallowed_STD3_mapped",[40,49324,41]],[[12821,12821],"disallowed_STD3_mapped",[40,50500,41]],[[12822,12822],"disallowed_STD3_mapped",[40,51088,41]],[[12823,12823],"disallowed_STD3_mapped",[40,52264,41]],[[12824,12824],"disallowed_STD3_mapped",[40,52852,41]],[[12825,12825],"disallowed_STD3_mapped",[40,53440,41]],[[12826,12826],"disallowed_STD3_mapped",[40,54028,41]],[[12827,12827],"disallowed_STD3_mapped",[40,54616,41]],[[12828,12828],"disallowed_STD3_mapped",[40,51452,41]],[[12829,12829],"disallowed_STD3_mapped",[40,50724,51204,41]],[[12830,12830],"disallowed_STD3_mapped",[40,50724,54980,41]],[[12831,12831],"disallowed"],[[12832,12832],"disallowed_STD3_mapped",[40,19968,41]],[[12833,12833],"disallowed_STD3_mapped",[40,20108,41]],[[12834,12834],"disallowed_STD3_mapped",[40,19977,41]],[[12835,12835],"disallowed_STD3_mapped",[40,22235,41]],[[12836,12836],"disallowed_STD3_mapped",[40,20116,41]],[[12837,12837],"disallowed_STD3_mapped",[40,20845,41]],[[12838,12838],"disallowed_STD3_mapped",[40,19971,41]],[[12839,12839],"disallowed_STD3_mapped",[40,20843,41]],[[12840,12840],"disallowed_STD3_mapped",[40,20061,41]],[[12841,12841],"disallowed_STD3_mapped",[40,21313,41]],[[12842,12842],"disallowed_STD3_mapped",[40,26376,41]],[[12843,12843],"disallowed_STD3_mapped",[40,28779,41]],[[12844,12844],"disallowed_STD3_mapped",[40,27700,41]],[[12845,12845],"disallowed_STD3_mapped",[40,26408,41]],[[12846,12846],"disallowed_STD3_mapped",[40,37329,41]],[[12847,12847],"disallowed_STD3_mapped",[40,22303,41]],[[12848,12848],"disallowed_STD3_mapped",[40,26085,41]],[[12849,12849],"disallowed_STD3_mapped",[40,26666,41]],[[12850,12850],"disallowed_STD3_mapped",[40,26377,41]],[[12851,12851],"disallowed_STD3_mapped",[40,31038,41]],[[12852,12852],"disallowed_STD3_mapped",[40,21517,41]],[[12853,12853],"disallowed_STD3_mapped",[40,29305,41]],[[12854,12854],"disallowed_STD3_mapped",[40,36001,41]],[[12855,12855],"disallowed_STD3_mapped",[40,31069,41]],[[12856,12856],"disallowed_STD3_mapped",[40,21172,41]],[[12857,12857],"disallowed_STD3_mapped",[40,20195,41]],[[12858,12858],"disallowed_STD3_mapped",[40,21628,41]],[[12859,12859],"disallowed_STD3_mapped",[40,23398,41]],[[12860,12860],"disallowed_STD3_mapped",[40,30435,41]],[[12861,12861],"disallowed_STD3_mapped",[40,20225,41]],[[12862,12862],"disallowed_STD3_mapped",[40,36039,41]],[[12863,12863],"disallowed_STD3_mapped",[40,21332,41]],[[12864,12864],"disallowed_STD3_mapped",[40,31085,41]],[[12865,12865],"disallowed_STD3_mapped",[40,20241,41]],[[12866,12866],"disallowed_STD3_mapped",[40,33258,41]],[[12867,12867],"disallowed_STD3_mapped",[40,33267,41]],[[12868,12868],"mapped",[21839]],[[12869,12869],"mapped",[24188]],[[12870,12870],"mapped",[25991]],[[12871,12871],"mapped",[31631]],[[12872,12879],"valid",[],"NV8"],[[12880,12880],"mapped",[112,116,101]],[[12881,12881],"mapped",[50,49]],[[12882,12882],"mapped",[50,50]],[[12883,12883],"mapped",[50,51]],[[12884,12884],"mapped",[50,52]],[[12885,12885],"mapped",[50,53]],[[12886,12886],"mapped",[50,54]],[[12887,12887],"mapped",[50,55]],[[12888,12888],"mapped",[50,56]],[[12889,12889],"mapped",[50,57]],[[12890,12890],"mapped",[51,48]],[[12891,12891],"mapped",[51,49]],[[12892,12892],"mapped",[51,50]],[[12893,12893],"mapped",[51,51]],[[12894,12894],"mapped",[51,52]],[[12895,12895],"mapped",[51,53]],[[12896,12896],"mapped",[4352]],[[12897,12897],"mapped",[4354]],[[12898,12898],"mapped",[4355]],[[12899,12899],"mapped",[4357]],[[12900,12900],"mapped",[4358]],[[12901,12901],"mapped",[4359]],[[12902,12902],"mapped",[4361]],[[12903,12903],"mapped",[4363]],[[12904,12904],"mapped",[4364]],[[12905,12905],"mapped",[4366]],[[12906,12906],"mapped",[4367]],[[12907,12907],"mapped",[4368]],[[12908,12908],"mapped",[4369]],[[12909,12909],"mapped",[4370]],[[12910,12910],"mapped",[44032]],[[12911,12911],"mapped",[45208]],[[12912,12912],"mapped",[45796]],[[12913,12913],"mapped",[46972]],[[12914,12914],"mapped",[47560]],[[12915,12915],"mapped",[48148]],[[12916,12916],"mapped",[49324]],[[12917,12917],"mapped",[50500]],[[12918,12918],"mapped",[51088]],[[12919,12919],"mapped",[52264]],[[12920,12920],"mapped",[52852]],[[12921,12921],"mapped",[53440]],[[12922,12922],"mapped",[54028]],[[12923,12923],"mapped",[54616]],[[12924,12924],"mapped",[52280,44256]],[[12925,12925],"mapped",[51452,51032]],[[12926,12926],"mapped",[50864]],[[12927,12927],"valid",[],"NV8"],[[12928,12928],"mapped",[19968]],[[12929,12929],"mapped",[20108]],[[12930,12930],"mapped",[19977]],[[12931,12931],"mapped",[22235]],[[12932,12932],"mapped",[20116]],[[12933,12933],"mapped",[20845]],[[12934,12934],"mapped",[19971]],[[12935,12935],"mapped",[20843]],[[12936,12936],"mapped",[20061]],[[12937,12937],"mapped",[21313]],[[12938,12938],"mapped",[26376]],[[12939,12939],"mapped",[28779]],[[12940,12940],"mapped",[27700]],[[12941,12941],"mapped",[26408]],[[12942,12942],"mapped",[37329]],[[12943,12943],"mapped",[22303]],[[12944,12944],"mapped",[26085]],[[12945,12945],"mapped",[26666]],[[12946,12946],"mapped",[26377]],[[12947,12947],"mapped",[31038]],[[12948,12948],"mapped",[21517]],[[12949,12949],"mapped",[29305]],[[12950,12950],"mapped",[36001]],[[12951,12951],"mapped",[31069]],[[12952,12952],"mapped",[21172]],[[12953,12953],"mapped",[31192]],[[12954,12954],"mapped",[30007]],[[12955,12955],"mapped",[22899]],[[12956,12956],"mapped",[36969]],[[12957,12957],"mapped",[20778]],[[12958,12958],"mapped",[21360]],[[12959,12959],"mapped",[27880]],[[12960,12960],"mapped",[38917]],[[12961,12961],"mapped",[20241]],[[12962,12962],"mapped",[20889]],[[12963,12963],"mapped",[27491]],[[12964,12964],"mapped",[19978]],[[12965,12965],"mapped",[20013]],[[12966,12966],"mapped",[19979]],[[12967,12967],"mapped",[24038]],[[12968,12968],"mapped",[21491]],[[12969,12969],"mapped",[21307]],[[12970,12970],"mapped",[23447]],[[12971,12971],"mapped",[23398]],[[12972,12972],"mapped",[30435]],[[12973,12973],"mapped",[20225]],[[12974,12974],"mapped",[36039]],[[12975,12975],"mapped",[21332]],[[12976,12976],"mapped",[22812]],[[12977,12977],"mapped",[51,54]],[[12978,12978],"mapped",[51,55]],[[12979,12979],"mapped",[51,56]],[[12980,12980],"mapped",[51,57]],[[12981,12981],"mapped",[52,48]],[[12982,12982],"mapped",[52,49]],[[12983,12983],"mapped",[52,50]],[[12984,12984],"mapped",[52,51]],[[12985,12985],"mapped",[52,52]],[[12986,12986],"mapped",[52,53]],[[12987,12987],"mapped",[52,54]],[[12988,12988],"mapped",[52,55]],[[12989,12989],"mapped",[52,56]],[[12990,12990],"mapped",[52,57]],[[12991,12991],"mapped",[53,48]],[[12992,12992],"mapped",[49,26376]],[[12993,12993],"mapped",[50,26376]],[[12994,12994],"mapped",[51,26376]],[[12995,12995],"mapped",[52,26376]],[[12996,12996],"mapped",[53,26376]],[[12997,12997],"mapped",[54,26376]],[[12998,12998],"mapped",[55,26376]],[[12999,12999],"mapped",[56,26376]],[[13000,13000],"mapped",[57,26376]],[[13001,13001],"mapped",[49,48,26376]],[[13002,13002],"mapped",[49,49,26376]],[[13003,13003],"mapped",[49,50,26376]],[[13004,13004],"mapped",[104,103]],[[13005,13005],"mapped",[101,114,103]],[[13006,13006],"mapped",[101,118]],[[13007,13007],"mapped",[108,116,100]],[[13008,13008],"mapped",[12450]],[[13009,13009],"mapped",[12452]],[[13010,13010],"mapped",[12454]],[[13011,13011],"mapped",[12456]],[[13012,13012],"mapped",[12458]],[[13013,13013],"mapped",[12459]],[[13014,13014],"mapped",[12461]],[[13015,13015],"mapped",[12463]],[[13016,13016],"mapped",[12465]],[[13017,13017],"mapped",[12467]],[[13018,13018],"mapped",[12469]],[[13019,13019],"mapped",[12471]],[[13020,13020],"mapped",[12473]],[[13021,13021],"mapped",[12475]],[[13022,13022],"mapped",[12477]],[[13023,13023],"mapped",[12479]],[[13024,13024],"mapped",[12481]],[[13025,13025],"mapped",[12484]],[[13026,13026],"mapped",[12486]],[[13027,13027],"mapped",[12488]],[[13028,13028],"mapped",[12490]],[[13029,13029],"mapped",[12491]],[[13030,13030],"mapped",[12492]],[[13031,13031],"mapped",[12493]],[[13032,13032],"mapped",[12494]],[[13033,13033],"mapped",[12495]],[[13034,13034],"mapped",[12498]],[[13035,13035],"mapped",[12501]],[[13036,13036],"mapped",[12504]],[[13037,13037],"mapped",[12507]],[[13038,13038],"mapped",[12510]],[[13039,13039],"mapped",[12511]],[[13040,13040],"mapped",[12512]],[[13041,13041],"mapped",[12513]],[[13042,13042],"mapped",[12514]],[[13043,13043],"mapped",[12516]],[[13044,13044],"mapped",[12518]],[[13045,13045],"mapped",[12520]],[[13046,13046],"mapped",[12521]],[[13047,13047],"mapped",[12522]],[[13048,13048],"mapped",[12523]],[[13049,13049],"mapped",[12524]],[[13050,13050],"mapped",[12525]],[[13051,13051],"mapped",[12527]],[[13052,13052],"mapped",[12528]],[[13053,13053],"mapped",[12529]],[[13054,13054],"mapped",[12530]],[[13055,13055],"disallowed"],[[13056,13056],"mapped",[12450,12497,12540,12488]],[[13057,13057],"mapped",[12450,12523,12501,12449]],[[13058,13058],"mapped",[12450,12531,12506,12450]],[[13059,13059],"mapped",[12450,12540,12523]],[[13060,13060],"mapped",[12452,12491,12531,12464]],[[13061,13061],"mapped",[12452,12531,12481]],[[13062,13062],"mapped",[12454,12457,12531]],[[13063,13063],"mapped",[12456,12473,12463,12540,12489]],[[13064,13064],"mapped",[12456,12540,12459,12540]],[[13065,13065],"mapped",[12458,12531,12473]],[[13066,13066],"mapped",[12458,12540,12512]],[[13067,13067],"mapped",[12459,12452,12522]],[[13068,13068],"mapped",[12459,12521,12483,12488]],[[13069,13069],"mapped",[12459,12525,12522,12540]],[[13070,13070],"mapped",[12460,12525,12531]],[[13071,13071],"mapped",[12460,12531,12510]],[[13072,13072],"mapped",[12462,12460]],[[13073,13073],"mapped",[12462,12491,12540]],[[13074,13074],"mapped",[12461,12517,12522,12540]],[[13075,13075],"mapped",[12462,12523,12480,12540]],[[13076,13076],"mapped",[12461,12525]],[[13077,13077],"mapped",[12461,12525,12464,12521,12512]],[[13078,13078],"mapped",[12461,12525,12513,12540,12488,12523]],[[13079,13079],"mapped",[12461,12525,12527,12483,12488]],[[13080,13080],"mapped",[12464,12521,12512]],[[13081,13081],"mapped",[12464,12521,12512,12488,12531]],[[13082,13082],"mapped",[12463,12523,12476,12452,12525]],[[13083,13083],"mapped",[12463,12525,12540,12493]],[[13084,13084],"mapped",[12465,12540,12473]],[[13085,13085],"mapped",[12467,12523,12490]],[[13086,13086],"mapped",[12467,12540,12509]],[[13087,13087],"mapped",[12469,12452,12463,12523]],[[13088,13088],"mapped",[12469,12531,12481,12540,12512]],[[13089,13089],"mapped",[12471,12522,12531,12464]],[[13090,13090],"mapped",[12475,12531,12481]],[[13091,13091],"mapped",[12475,12531,12488]],[[13092,13092],"mapped",[12480,12540,12473]],[[13093,13093],"mapped",[12487,12471]],[[13094,13094],"mapped",[12489,12523]],[[13095,13095],"mapped",[12488,12531]],[[13096,13096],"mapped",[12490,12494]],[[13097,13097],"mapped",[12494,12483,12488]],[[13098,13098],"mapped",[12495,12452,12484]],[[13099,13099],"mapped",[12497,12540,12475,12531,12488]],[[13100,13100],"mapped",[12497,12540,12484]],[[13101,13101],"mapped",[12496,12540,12524,12523]],[[13102,13102],"mapped",[12500,12450,12473,12488,12523]],[[13103,13103],"mapped",[12500,12463,12523]],[[13104,13104],"mapped",[12500,12467]],[[13105,13105],"mapped",[12499,12523]],[[13106,13106],"mapped",[12501,12449,12521,12483,12489]],[[13107,13107],"mapped",[12501,12451,12540,12488]],[[13108,13108],"mapped",[12502,12483,12471,12455,12523]],[[13109,13109],"mapped",[12501,12521,12531]],[[13110,13110],"mapped",[12504,12463,12479,12540,12523]],[[13111,13111],"mapped",[12506,12477]],[[13112,13112],"mapped",[12506,12491,12498]],[[13113,13113],"mapped",[12504,12523,12484]],[[13114,13114],"mapped",[12506,12531,12473]],[[13115,13115],"mapped",[12506,12540,12472]],[[13116,13116],"mapped",[12505,12540,12479]],[[13117,13117],"mapped",[12509,12452,12531,12488]],[[13118,13118],"mapped",[12508,12523,12488]],[[13119,13119],"mapped",[12507,12531]],[[13120,13120],"mapped",[12509,12531,12489]],[[13121,13121],"mapped",[12507,12540,12523]],[[13122,13122],"mapped",[12507,12540,12531]],[[13123,13123],"mapped",[12510,12452,12463,12525]],[[13124,13124],"mapped",[12510,12452,12523]],[[13125,13125],"mapped",[12510,12483,12495]],[[13126,13126],"mapped",[12510,12523,12463]],[[13127,13127],"mapped",[12510,12531,12471,12519,12531]],[[13128,13128],"mapped",[12511,12463,12525,12531]],[[13129,13129],"mapped",[12511,12522]],[[13130,13130],"mapped",[12511,12522,12496,12540,12523]],[[13131,13131],"mapped",[12513,12460]],[[13132,13132],"mapped",[12513,12460,12488,12531]],[[13133,13133],"mapped",[12513,12540,12488,12523]],[[13134,13134],"mapped",[12516,12540,12489]],[[13135,13135],"mapped",[12516,12540,12523]],[[13136,13136],"mapped",[12518,12450,12531]],[[13137,13137],"mapped",[12522,12483,12488,12523]],[[13138,13138],"mapped",[12522,12521]],[[13139,13139],"mapped",[12523,12500,12540]],[[13140,13140],"mapped",[12523,12540,12502,12523]],[[13141,13141],"mapped",[12524,12512]],[[13142,13142],"mapped",[12524,12531,12488,12466,12531]],[[13143,13143],"mapped",[12527,12483,12488]],[[13144,13144],"mapped",[48,28857]],[[13145,13145],"mapped",[49,28857]],[[13146,13146],"mapped",[50,28857]],[[13147,13147],"mapped",[51,28857]],[[13148,13148],"mapped",[52,28857]],[[13149,13149],"mapped",[53,28857]],[[13150,13150],"mapped",[54,28857]],[[13151,13151],"mapped",[55,28857]],[[13152,13152],"mapped",[56,28857]],[[13153,13153],"mapped",[57,28857]],[[13154,13154],"mapped",[49,48,28857]],[[13155,13155],"mapped",[49,49,28857]],[[13156,13156],"mapped",[49,50,28857]],[[13157,13157],"mapped",[49,51,28857]],[[13158,13158],"mapped",[49,52,28857]],[[13159,13159],"mapped",[49,53,28857]],[[13160,13160],"mapped",[49,54,28857]],[[13161,13161],"mapped",[49,55,28857]],[[13162,13162],"mapped",[49,56,28857]],[[13163,13163],"mapped",[49,57,28857]],[[13164,13164],"mapped",[50,48,28857]],[[13165,13165],"mapped",[50,49,28857]],[[13166,13166],"mapped",[50,50,28857]],[[13167,13167],"mapped",[50,51,28857]],[[13168,13168],"mapped",[50,52,28857]],[[13169,13169],"mapped",[104,112,97]],[[13170,13170],"mapped",[100,97]],[[13171,13171],"mapped",[97,117]],[[13172,13172],"mapped",[98,97,114]],[[13173,13173],"mapped",[111,118]],[[13174,13174],"mapped",[112,99]],[[13175,13175],"mapped",[100,109]],[[13176,13176],"mapped",[100,109,50]],[[13177,13177],"mapped",[100,109,51]],[[13178,13178],"mapped",[105,117]],[[13179,13179],"mapped",[24179,25104]],[[13180,13180],"mapped",[26157,21644]],[[13181,13181],"mapped",[22823,27491]],[[13182,13182],"mapped",[26126,27835]],[[13183,13183],"mapped",[26666,24335,20250,31038]],[[13184,13184],"mapped",[112,97]],[[13185,13185],"mapped",[110,97]],[[13186,13186],"mapped",[956,97]],[[13187,13187],"mapped",[109,97]],[[13188,13188],"mapped",[107,97]],[[13189,13189],"mapped",[107,98]],[[13190,13190],"mapped",[109,98]],[[13191,13191],"mapped",[103,98]],[[13192,13192],"mapped",[99,97,108]],[[13193,13193],"mapped",[107,99,97,108]],[[13194,13194],"mapped",[112,102]],[[13195,13195],"mapped",[110,102]],[[13196,13196],"mapped",[956,102]],[[13197,13197],"mapped",[956,103]],[[13198,13198],"mapped",[109,103]],[[13199,13199],"mapped",[107,103]],[[13200,13200],"mapped",[104,122]],[[13201,13201],"mapped",[107,104,122]],[[13202,13202],"mapped",[109,104,122]],[[13203,13203],"mapped",[103,104,122]],[[13204,13204],"mapped",[116,104,122]],[[13205,13205],"mapped",[956,108]],[[13206,13206],"mapped",[109,108]],[[13207,13207],"mapped",[100,108]],[[13208,13208],"mapped",[107,108]],[[13209,13209],"mapped",[102,109]],[[13210,13210],"mapped",[110,109]],[[13211,13211],"mapped",[956,109]],[[13212,13212],"mapped",[109,109]],[[13213,13213],"mapped",[99,109]],[[13214,13214],"mapped",[107,109]],[[13215,13215],"mapped",[109,109,50]],[[13216,13216],"mapped",[99,109,50]],[[13217,13217],"mapped",[109,50]],[[13218,13218],"mapped",[107,109,50]],[[13219,13219],"mapped",[109,109,51]],[[13220,13220],"mapped",[99,109,51]],[[13221,13221],"mapped",[109,51]],[[13222,13222],"mapped",[107,109,51]],[[13223,13223],"mapped",[109,8725,115]],[[13224,13224],"mapped",[109,8725,115,50]],[[13225,13225],"mapped",[112,97]],[[13226,13226],"mapped",[107,112,97]],[[13227,13227],"mapped",[109,112,97]],[[13228,13228],"mapped",[103,112,97]],[[13229,13229],"mapped",[114,97,100]],[[13230,13230],"mapped",[114,97,100,8725,115]],[[13231,13231],"mapped",[114,97,100,8725,115,50]],[[13232,13232],"mapped",[112,115]],[[13233,13233],"mapped",[110,115]],[[13234,13234],"mapped",[956,115]],[[13235,13235],"mapped",[109,115]],[[13236,13236],"mapped",[112,118]],[[13237,13237],"mapped",[110,118]],[[13238,13238],"mapped",[956,118]],[[13239,13239],"mapped",[109,118]],[[13240,13240],"mapped",[107,118]],[[13241,13241],"mapped",[109,118]],[[13242,13242],"mapped",[112,119]],[[13243,13243],"mapped",[110,119]],[[13244,13244],"mapped",[956,119]],[[13245,13245],"mapped",[109,119]],[[13246,13246],"mapped",[107,119]],[[13247,13247],"mapped",[109,119]],[[13248,13248],"mapped",[107,969]],[[13249,13249],"mapped",[109,969]],[[13250,13250],"disallowed"],[[13251,13251],"mapped",[98,113]],[[13252,13252],"mapped",[99,99]],[[13253,13253],"mapped",[99,100]],[[13254,13254],"mapped",[99,8725,107,103]],[[13255,13255],"disallowed"],[[13256,13256],"mapped",[100,98]],[[13257,13257],"mapped",[103,121]],[[13258,13258],"mapped",[104,97]],[[13259,13259],"mapped",[104,112]],[[13260,13260],"mapped",[105,110]],[[13261,13261],"mapped",[107,107]],[[13262,13262],"mapped",[107,109]],[[13263,13263],"mapped",[107,116]],[[13264,13264],"mapped",[108,109]],[[13265,13265],"mapped",[108,110]],[[13266,13266],"mapped",[108,111,103]],[[13267,13267],"mapped",[108,120]],[[13268,13268],"mapped",[109,98]],[[13269,13269],"mapped",[109,105,108]],[[13270,13270],"mapped",[109,111,108]],[[13271,13271],"mapped",[112,104]],[[13272,13272],"disallowed"],[[13273,13273],"mapped",[112,112,109]],[[13274,13274],"mapped",[112,114]],[[13275,13275],"mapped",[115,114]],[[13276,13276],"mapped",[115,118]],[[13277,13277],"mapped",[119,98]],[[13278,13278],"mapped",[118,8725,109]],[[13279,13279],"mapped",[97,8725,109]],[[13280,13280],"mapped",[49,26085]],[[13281,13281],"mapped",[50,26085]],[[13282,13282],"mapped",[51,26085]],[[13283,13283],"mapped",[52,26085]],[[13284,13284],"mapped",[53,26085]],[[13285,13285],"mapped",[54,26085]],[[13286,13286],"mapped",[55,26085]],[[13287,13287],"mapped",[56,26085]],[[13288,13288],"mapped",[57,26085]],[[13289,13289],"mapped",[49,48,26085]],[[13290,13290],"mapped",[49,49,26085]],[[13291,13291],"mapped",[49,50,26085]],[[13292,13292],"mapped",[49,51,26085]],[[13293,13293],"mapped",[49,52,26085]],[[13294,13294],"mapped",[49,53,26085]],[[13295,13295],"mapped",[49,54,26085]],[[13296,13296],"mapped",[49,55,26085]],[[13297,13297],"mapped",[49,56,26085]],[[13298,13298],"mapped",[49,57,26085]],[[13299,13299],"mapped",[50,48,26085]],[[13300,13300],"mapped",[50,49,26085]],[[13301,13301],"mapped",[50,50,26085]],[[13302,13302],"mapped",[50,51,26085]],[[13303,13303],"mapped",[50,52,26085]],[[13304,13304],"mapped",[50,53,26085]],[[13305,13305],"mapped",[50,54,26085]],[[13306,13306],"mapped",[50,55,26085]],[[13307,13307],"mapped",[50,56,26085]],[[13308,13308],"mapped",[50,57,26085]],[[13309,13309],"mapped",[51,48,26085]],[[13310,13310],"mapped",[51,49,26085]],[[13311,13311],"mapped",[103,97,108]],[[13312,19893],"valid"],[[19894,19903],"disallowed"],[[19904,19967],"valid",[],"NV8"],[[19968,40869],"valid"],[[40870,40891],"valid"],[[40892,40899],"valid"],[[40900,40907],"valid"],[[40908,40908],"valid"],[[40909,40917],"valid"],[[40918,40959],"disallowed"],[[40960,42124],"valid"],[[42125,42127],"disallowed"],[[42128,42145],"valid",[],"NV8"],[[42146,42147],"valid",[],"NV8"],[[42148,42163],"valid",[],"NV8"],[[42164,42164],"valid",[],"NV8"],[[42165,42176],"valid",[],"NV8"],[[42177,42177],"valid",[],"NV8"],[[42178,42180],"valid",[],"NV8"],[[42181,42181],"valid",[],"NV8"],[[42182,42182],"valid",[],"NV8"],[[42183,42191],"disallowed"],[[42192,42237],"valid"],[[42238,42239],"valid",[],"NV8"],[[42240,42508],"valid"],[[42509,42511],"valid",[],"NV8"],[[42512,42539],"valid"],[[42540,42559],"disallowed"],[[42560,42560],"mapped",[42561]],[[42561,42561],"valid"],[[42562,42562],"mapped",[42563]],[[42563,42563],"valid"],[[42564,42564],"mapped",[42565]],[[42565,42565],"valid"],[[42566,42566],"mapped",[42567]],[[42567,42567],"valid"],[[42568,42568],"mapped",[42569]],[[42569,42569],"valid"],[[42570,42570],"mapped",[42571]],[[42571,42571],"valid"],[[42572,42572],"mapped",[42573]],[[42573,42573],"valid"],[[42574,42574],"mapped",[42575]],[[42575,42575],"valid"],[[42576,42576],"mapped",[42577]],[[42577,42577],"valid"],[[42578,42578],"mapped",[42579]],[[42579,42579],"valid"],[[42580,42580],"mapped",[42581]],[[42581,42581],"valid"],[[42582,42582],"mapped",[42583]],[[42583,42583],"valid"],[[42584,42584],"mapped",[42585]],[[42585,42585],"valid"],[[42586,42586],"mapped",[42587]],[[42587,42587],"valid"],[[42588,42588],"mapped",[42589]],[[42589,42589],"valid"],[[42590,42590],"mapped",[42591]],[[42591,42591],"valid"],[[42592,42592],"mapped",[42593]],[[42593,42593],"valid"],[[42594,42594],"mapped",[42595]],[[42595,42595],"valid"],[[42596,42596],"mapped",[42597]],[[42597,42597],"valid"],[[42598,42598],"mapped",[42599]],[[42599,42599],"valid"],[[42600,42600],"mapped",[42601]],[[42601,42601],"valid"],[[42602,42602],"mapped",[42603]],[[42603,42603],"valid"],[[42604,42604],"mapped",[42605]],[[42605,42607],"valid"],[[42608,42611],"valid",[],"NV8"],[[42612,42619],"valid"],[[42620,42621],"valid"],[[42622,42622],"valid",[],"NV8"],[[42623,42623],"valid"],[[42624,42624],"mapped",[42625]],[[42625,42625],"valid"],[[42626,42626],"mapped",[42627]],[[42627,42627],"valid"],[[42628,42628],"mapped",[42629]],[[42629,42629],"valid"],[[42630,42630],"mapped",[42631]],[[42631,42631],"valid"],[[42632,42632],"mapped",[42633]],[[42633,42633],"valid"],[[42634,42634],"mapped",[42635]],[[42635,42635],"valid"],[[42636,42636],"mapped",[42637]],[[42637,42637],"valid"],[[42638,42638],"mapped",[42639]],[[42639,42639],"valid"],[[42640,42640],"mapped",[42641]],[[42641,42641],"valid"],[[42642,42642],"mapped",[42643]],[[42643,42643],"valid"],[[42644,42644],"mapped",[42645]],[[42645,42645],"valid"],[[42646,42646],"mapped",[42647]],[[42647,42647],"valid"],[[42648,42648],"mapped",[42649]],[[42649,42649],"valid"],[[42650,42650],"mapped",[42651]],[[42651,42651],"valid"],[[42652,42652],"mapped",[1098]],[[42653,42653],"mapped",[1100]],[[42654,42654],"valid"],[[42655,42655],"valid"],[[42656,42725],"valid"],[[42726,42735],"valid",[],"NV8"],[[42736,42737],"valid"],[[42738,42743],"valid",[],"NV8"],[[42744,42751],"disallowed"],[[42752,42774],"valid",[],"NV8"],[[42775,42778],"valid"],[[42779,42783],"valid"],[[42784,42785],"valid",[],"NV8"],[[42786,42786],"mapped",[42787]],[[42787,42787],"valid"],[[42788,42788],"mapped",[42789]],[[42789,42789],"valid"],[[42790,42790],"mapped",[42791]],[[42791,42791],"valid"],[[42792,42792],"mapped",[42793]],[[42793,42793],"valid"],[[42794,42794],"mapped",[42795]],[[42795,42795],"valid"],[[42796,42796],"mapped",[42797]],[[42797,42797],"valid"],[[42798,42798],"mapped",[42799]],[[42799,42801],"valid"],[[42802,42802],"mapped",[42803]],[[42803,42803],"valid"],[[42804,42804],"mapped",[42805]],[[42805,42805],"valid"],[[42806,42806],"mapped",[42807]],[[42807,42807],"valid"],[[42808,42808],"mapped",[42809]],[[42809,42809],"valid"],[[42810,42810],"mapped",[42811]],[[42811,42811],"valid"],[[42812,42812],"mapped",[42813]],[[42813,42813],"valid"],[[42814,42814],"mapped",[42815]],[[42815,42815],"valid"],[[42816,42816],"mapped",[42817]],[[42817,42817],"valid"],[[42818,42818],"mapped",[42819]],[[42819,42819],"valid"],[[42820,42820],"mapped",[42821]],[[42821,42821],"valid"],[[42822,42822],"mapped",[42823]],[[42823,42823],"valid"],[[42824,42824],"mapped",[42825]],[[42825,42825],"valid"],[[42826,42826],"mapped",[42827]],[[42827,42827],"valid"],[[42828,42828],"mapped",[42829]],[[42829,42829],"valid"],[[42830,42830],"mapped",[42831]],[[42831,42831],"valid"],[[42832,42832],"mapped",[42833]],[[42833,42833],"valid"],[[42834,42834],"mapped",[42835]],[[42835,42835],"valid"],[[42836,42836],"mapped",[42837]],[[42837,42837],"valid"],[[42838,42838],"mapped",[42839]],[[42839,42839],"valid"],[[42840,42840],"mapped",[42841]],[[42841,42841],"valid"],[[42842,42842],"mapped",[42843]],[[42843,42843],"valid"],[[42844,42844],"mapped",[42845]],[[42845,42845],"valid"],[[42846,42846],"mapped",[42847]],[[42847,42847],"valid"],[[42848,42848],"mapped",[42849]],[[42849,42849],"valid"],[[42850,42850],"mapped",[42851]],[[42851,42851],"valid"],[[42852,42852],"mapped",[42853]],[[42853,42853],"valid"],[[42854,42854],"mapped",[42855]],[[42855,42855],"valid"],[[42856,42856],"mapped",[42857]],[[42857,42857],"valid"],[[42858,42858],"mapped",[42859]],[[42859,42859],"valid"],[[42860,42860],"mapped",[42861]],[[42861,42861],"valid"],[[42862,42862],"mapped",[42863]],[[42863,42863],"valid"],[[42864,42864],"mapped",[42863]],[[42865,42872],"valid"],[[42873,42873],"mapped",[42874]],[[42874,42874],"valid"],[[42875,42875],"mapped",[42876]],[[42876,42876],"valid"],[[42877,42877],"mapped",[7545]],[[42878,42878],"mapped",[42879]],[[42879,42879],"valid"],[[42880,42880],"mapped",[42881]],[[42881,42881],"valid"],[[42882,42882],"mapped",[42883]],[[42883,42883],"valid"],[[42884,42884],"mapped",[42885]],[[42885,42885],"valid"],[[42886,42886],"mapped",[42887]],[[42887,42888],"valid"],[[42889,42890],"valid",[],"NV8"],[[42891,42891],"mapped",[42892]],[[42892,42892],"valid"],[[42893,42893],"mapped",[613]],[[42894,42894],"valid"],[[42895,42895],"valid"],[[42896,42896],"mapped",[42897]],[[42897,42897],"valid"],[[42898,42898],"mapped",[42899]],[[42899,42899],"valid"],[[42900,42901],"valid"],[[42902,42902],"mapped",[42903]],[[42903,42903],"valid"],[[42904,42904],"mapped",[42905]],[[42905,42905],"valid"],[[42906,42906],"mapped",[42907]],[[42907,42907],"valid"],[[42908,42908],"mapped",[42909]],[[42909,42909],"valid"],[[42910,42910],"mapped",[42911]],[[42911,42911],"valid"],[[42912,42912],"mapped",[42913]],[[42913,42913],"valid"],[[42914,42914],"mapped",[42915]],[[42915,42915],"valid"],[[42916,42916],"mapped",[42917]],[[42917,42917],"valid"],[[42918,42918],"mapped",[42919]],[[42919,42919],"valid"],[[42920,42920],"mapped",[42921]],[[42921,42921],"valid"],[[42922,42922],"mapped",[614]],[[42923,42923],"mapped",[604]],[[42924,42924],"mapped",[609]],[[42925,42925],"mapped",[620]],[[42926,42927],"disallowed"],[[42928,42928],"mapped",[670]],[[42929,42929],"mapped",[647]],[[42930,42930],"mapped",[669]],[[42931,42931],"mapped",[43859]],[[42932,42932],"mapped",[42933]],[[42933,42933],"valid"],[[42934,42934],"mapped",[42935]],[[42935,42935],"valid"],[[42936,42998],"disallowed"],[[42999,42999],"valid"],[[43000,43000],"mapped",[295]],[[43001,43001],"mapped",[339]],[[43002,43002],"valid"],[[43003,43007],"valid"],[[43008,43047],"valid"],[[43048,43051],"valid",[],"NV8"],[[43052,43055],"disallowed"],[[43056,43065],"valid",[],"NV8"],[[43066,43071],"disallowed"],[[43072,43123],"valid"],[[43124,43127],"valid",[],"NV8"],[[43128,43135],"disallowed"],[[43136,43204],"valid"],[[43205,43213],"disallowed"],[[43214,43215],"valid",[],"NV8"],[[43216,43225],"valid"],[[43226,43231],"disallowed"],[[43232,43255],"valid"],[[43256,43258],"valid",[],"NV8"],[[43259,43259],"valid"],[[43260,43260],"valid",[],"NV8"],[[43261,43261],"valid"],[[43262,43263],"disallowed"],[[43264,43309],"valid"],[[43310,43311],"valid",[],"NV8"],[[43312,43347],"valid"],[[43348,43358],"disallowed"],[[43359,43359],"valid",[],"NV8"],[[43360,43388],"valid",[],"NV8"],[[43389,43391],"disallowed"],[[43392,43456],"valid"],[[43457,43469],"valid",[],"NV8"],[[43470,43470],"disallowed"],[[43471,43481],"valid"],[[43482,43485],"disallowed"],[[43486,43487],"valid",[],"NV8"],[[43488,43518],"valid"],[[43519,43519],"disallowed"],[[43520,43574],"valid"],[[43575,43583],"disallowed"],[[43584,43597],"valid"],[[43598,43599],"disallowed"],[[43600,43609],"valid"],[[43610,43611],"disallowed"],[[43612,43615],"valid",[],"NV8"],[[43616,43638],"valid"],[[43639,43641],"valid",[],"NV8"],[[43642,43643],"valid"],[[43644,43647],"valid"],[[43648,43714],"valid"],[[43715,43738],"disallowed"],[[43739,43741],"valid"],[[43742,43743],"valid",[],"NV8"],[[43744,43759],"valid"],[[43760,43761],"valid",[],"NV8"],[[43762,43766],"valid"],[[43767,43776],"disallowed"],[[43777,43782],"valid"],[[43783,43784],"disallowed"],[[43785,43790],"valid"],[[43791,43792],"disallowed"],[[43793,43798],"valid"],[[43799,43807],"disallowed"],[[43808,43814],"valid"],[[43815,43815],"disallowed"],[[43816,43822],"valid"],[[43823,43823],"disallowed"],[[43824,43866],"valid"],[[43867,43867],"valid",[],"NV8"],[[43868,43868],"mapped",[42791]],[[43869,43869],"mapped",[43831]],[[43870,43870],"mapped",[619]],[[43871,43871],"mapped",[43858]],[[43872,43875],"valid"],[[43876,43877],"valid"],[[43878,43887],"disallowed"],[[43888,43888],"mapped",[5024]],[[43889,43889],"mapped",[5025]],[[43890,43890],"mapped",[5026]],[[43891,43891],"mapped",[5027]],[[43892,43892],"mapped",[5028]],[[43893,43893],"mapped",[5029]],[[43894,43894],"mapped",[5030]],[[43895,43895],"mapped",[5031]],[[43896,43896],"mapped",[5032]],[[43897,43897],"mapped",[5033]],[[43898,43898],"mapped",[5034]],[[43899,43899],"mapped",[5035]],[[43900,43900],"mapped",[5036]],[[43901,43901],"mapped",[5037]],[[43902,43902],"mapped",[5038]],[[43903,43903],"mapped",[5039]],[[43904,43904],"mapped",[5040]],[[43905,43905],"mapped",[5041]],[[43906,43906],"mapped",[5042]],[[43907,43907],"mapped",[5043]],[[43908,43908],"mapped",[5044]],[[43909,43909],"mapped",[5045]],[[43910,43910],"mapped",[5046]],[[43911,43911],"mapped",[5047]],[[43912,43912],"mapped",[5048]],[[43913,43913],"mapped",[5049]],[[43914,43914],"mapped",[5050]],[[43915,43915],"mapped",[5051]],[[43916,43916],"mapped",[5052]],[[43917,43917],"mapped",[5053]],[[43918,43918],"mapped",[5054]],[[43919,43919],"mapped",[5055]],[[43920,43920],"mapped",[5056]],[[43921,43921],"mapped",[5057]],[[43922,43922],"mapped",[5058]],[[43923,43923],"mapped",[5059]],[[43924,43924],"mapped",[5060]],[[43925,43925],"mapped",[5061]],[[43926,43926],"mapped",[5062]],[[43927,43927],"mapped",[5063]],[[43928,43928],"mapped",[5064]],[[43929,43929],"mapped",[5065]],[[43930,43930],"mapped",[5066]],[[43931,43931],"mapped",[5067]],[[43932,43932],"mapped",[5068]],[[43933,43933],"mapped",[5069]],[[43934,43934],"mapped",[5070]],[[43935,43935],"mapped",[5071]],[[43936,43936],"mapped",[5072]],[[43937,43937],"mapped",[5073]],[[43938,43938],"mapped",[5074]],[[43939,43939],"mapped",[5075]],[[43940,43940],"mapped",[5076]],[[43941,43941],"mapped",[5077]],[[43942,43942],"mapped",[5078]],[[43943,43943],"mapped",[5079]],[[43944,43944],"mapped",[5080]],[[43945,43945],"mapped",[5081]],[[43946,43946],"mapped",[5082]],[[43947,43947],"mapped",[5083]],[[43948,43948],"mapped",[5084]],[[43949,43949],"mapped",[5085]],[[43950,43950],"mapped",[5086]],[[43951,43951],"mapped",[5087]],[[43952,43952],"mapped",[5088]],[[43953,43953],"mapped",[5089]],[[43954,43954],"mapped",[5090]],[[43955,43955],"mapped",[5091]],[[43956,43956],"mapped",[5092]],[[43957,43957],"mapped",[5093]],[[43958,43958],"mapped",[5094]],[[43959,43959],"mapped",[5095]],[[43960,43960],"mapped",[5096]],[[43961,43961],"mapped",[5097]],[[43962,43962],"mapped",[5098]],[[43963,43963],"mapped",[5099]],[[43964,43964],"mapped",[5100]],[[43965,43965],"mapped",[5101]],[[43966,43966],"mapped",[5102]],[[43967,43967],"mapped",[5103]],[[43968,44010],"valid"],[[44011,44011],"valid",[],"NV8"],[[44012,44013],"valid"],[[44014,44015],"disallowed"],[[44016,44025],"valid"],[[44026,44031],"disallowed"],[[44032,55203],"valid"],[[55204,55215],"disallowed"],[[55216,55238],"valid",[],"NV8"],[[55239,55242],"disallowed"],[[55243,55291],"valid",[],"NV8"],[[55292,55295],"disallowed"],[[55296,57343],"disallowed"],[[57344,63743],"disallowed"],[[63744,63744],"mapped",[35912]],[[63745,63745],"mapped",[26356]],[[63746,63746],"mapped",[36554]],[[63747,63747],"mapped",[36040]],[[63748,63748],"mapped",[28369]],[[63749,63749],"mapped",[20018]],[[63750,63750],"mapped",[21477]],[[63751,63752],"mapped",[40860]],[[63753,63753],"mapped",[22865]],[[63754,63754],"mapped",[37329]],[[63755,63755],"mapped",[21895]],[[63756,63756],"mapped",[22856]],[[63757,63757],"mapped",[25078]],[[63758,63758],"mapped",[30313]],[[63759,63759],"mapped",[32645]],[[63760,63760],"mapped",[34367]],[[63761,63761],"mapped",[34746]],[[63762,63762],"mapped",[35064]],[[63763,63763],"mapped",[37007]],[[63764,63764],"mapped",[27138]],[[63765,63765],"mapped",[27931]],[[63766,63766],"mapped",[28889]],[[63767,63767],"mapped",[29662]],[[63768,63768],"mapped",[33853]],[[63769,63769],"mapped",[37226]],[[63770,63770],"mapped",[39409]],[[63771,63771],"mapped",[20098]],[[63772,63772],"mapped",[21365]],[[63773,63773],"mapped",[27396]],[[63774,63774],"mapped",[29211]],[[63775,63775],"mapped",[34349]],[[63776,63776],"mapped",[40478]],[[63777,63777],"mapped",[23888]],[[63778,63778],"mapped",[28651]],[[63779,63779],"mapped",[34253]],[[63780,63780],"mapped",[35172]],[[63781,63781],"mapped",[25289]],[[63782,63782],"mapped",[33240]],[[63783,63783],"mapped",[34847]],[[63784,63784],"mapped",[24266]],[[63785,63785],"mapped",[26391]],[[63786,63786],"mapped",[28010]],[[63787,63787],"mapped",[29436]],[[63788,63788],"mapped",[37070]],[[63789,63789],"mapped",[20358]],[[63790,63790],"mapped",[20919]],[[63791,63791],"mapped",[21214]],[[63792,63792],"mapped",[25796]],[[63793,63793],"mapped",[27347]],[[63794,63794],"mapped",[29200]],[[63795,63795],"mapped",[30439]],[[63796,63796],"mapped",[32769]],[[63797,63797],"mapped",[34310]],[[63798,63798],"mapped",[34396]],[[63799,63799],"mapped",[36335]],[[63800,63800],"mapped",[38706]],[[63801,63801],"mapped",[39791]],[[63802,63802],"mapped",[40442]],[[63803,63803],"mapped",[30860]],[[63804,63804],"mapped",[31103]],[[63805,63805],"mapped",[32160]],[[63806,63806],"mapped",[33737]],[[63807,63807],"mapped",[37636]],[[63808,63808],"mapped",[40575]],[[63809,63809],"mapped",[35542]],[[63810,63810],"mapped",[22751]],[[63811,63811],"mapped",[24324]],[[63812,63812],"mapped",[31840]],[[63813,63813],"mapped",[32894]],[[63814,63814],"mapped",[29282]],[[63815,63815],"mapped",[30922]],[[63816,63816],"mapped",[36034]],[[63817,63817],"mapped",[38647]],[[63818,63818],"mapped",[22744]],[[63819,63819],"mapped",[23650]],[[63820,63820],"mapped",[27155]],[[63821,63821],"mapped",[28122]],[[63822,63822],"mapped",[28431]],[[63823,63823],"mapped",[32047]],[[63824,63824],"mapped",[32311]],[[63825,63825],"mapped",[38475]],[[63826,63826],"mapped",[21202]],[[63827,63827],"mapped",[32907]],[[63828,63828],"mapped",[20956]],[[63829,63829],"mapped",[20940]],[[63830,63830],"mapped",[31260]],[[63831,63831],"mapped",[32190]],[[63832,63832],"mapped",[33777]],[[63833,63833],"mapped",[38517]],[[63834,63834],"mapped",[35712]],[[63835,63835],"mapped",[25295]],[[63836,63836],"mapped",[27138]],[[63837,63837],"mapped",[35582]],[[63838,63838],"mapped",[20025]],[[63839,63839],"mapped",[23527]],[[63840,63840],"mapped",[24594]],[[63841,63841],"mapped",[29575]],[[63842,63842],"mapped",[30064]],[[63843,63843],"mapped",[21271]],[[63844,63844],"mapped",[30971]],[[63845,63845],"mapped",[20415]],[[63846,63846],"mapped",[24489]],[[63847,63847],"mapped",[19981]],[[63848,63848],"mapped",[27852]],[[63849,63849],"mapped",[25976]],[[63850,63850],"mapped",[32034]],[[63851,63851],"mapped",[21443]],[[63852,63852],"mapped",[22622]],[[63853,63853],"mapped",[30465]],[[63854,63854],"mapped",[33865]],[[63855,63855],"mapped",[35498]],[[63856,63856],"mapped",[27578]],[[63857,63857],"mapped",[36784]],[[63858,63858],"mapped",[27784]],[[63859,63859],"mapped",[25342]],[[63860,63860],"mapped",[33509]],[[63861,63861],"mapped",[25504]],[[63862,63862],"mapped",[30053]],[[63863,63863],"mapped",[20142]],[[63864,63864],"mapped",[20841]],[[63865,63865],"mapped",[20937]],[[63866,63866],"mapped",[26753]],[[63867,63867],"mapped",[31975]],[[63868,63868],"mapped",[33391]],[[63869,63869],"mapped",[35538]],[[63870,63870],"mapped",[37327]],[[63871,63871],"mapped",[21237]],[[63872,63872],"mapped",[21570]],[[63873,63873],"mapped",[22899]],[[63874,63874],"mapped",[24300]],[[63875,63875],"mapped",[26053]],[[63876,63876],"mapped",[28670]],[[63877,63877],"mapped",[31018]],[[63878,63878],"mapped",[38317]],[[63879,63879],"mapped",[39530]],[[63880,63880],"mapped",[40599]],[[63881,63881],"mapped",[40654]],[[63882,63882],"mapped",[21147]],[[63883,63883],"mapped",[26310]],[[63884,63884],"mapped",[27511]],[[63885,63885],"mapped",[36706]],[[63886,63886],"mapped",[24180]],[[63887,63887],"mapped",[24976]],[[63888,63888],"mapped",[25088]],[[63889,63889],"mapped",[25754]],[[63890,63890],"mapped",[28451]],[[63891,63891],"mapped",[29001]],[[63892,63892],"mapped",[29833]],[[63893,63893],"mapped",[31178]],[[63894,63894],"mapped",[32244]],[[63895,63895],"mapped",[32879]],[[63896,63896],"mapped",[36646]],[[63897,63897],"mapped",[34030]],[[63898,63898],"mapped",[36899]],[[63899,63899],"mapped",[37706]],[[63900,63900],"mapped",[21015]],[[63901,63901],"mapped",[21155]],[[63902,63902],"mapped",[21693]],[[63903,63903],"mapped",[28872]],[[63904,63904],"mapped",[35010]],[[63905,63905],"mapped",[35498]],[[63906,63906],"mapped",[24265]],[[63907,63907],"mapped",[24565]],[[63908,63908],"mapped",[25467]],[[63909,63909],"mapped",[27566]],[[63910,63910],"mapped",[31806]],[[63911,63911],"mapped",[29557]],[[63912,63912],"mapped",[20196]],[[63913,63913],"mapped",[22265]],[[63914,63914],"mapped",[23527]],[[63915,63915],"mapped",[23994]],[[63916,63916],"mapped",[24604]],[[63917,63917],"mapped",[29618]],[[63918,63918],"mapped",[29801]],[[63919,63919],"mapped",[32666]],[[63920,63920],"mapped",[32838]],[[63921,63921],"mapped",[37428]],[[63922,63922],"mapped",[38646]],[[63923,63923],"mapped",[38728]],[[63924,63924],"mapped",[38936]],[[63925,63925],"mapped",[20363]],[[63926,63926],"mapped",[31150]],[[63927,63927],"mapped",[37300]],[[63928,63928],"mapped",[38584]],[[63929,63929],"mapped",[24801]],[[63930,63930],"mapped",[20102]],[[63931,63931],"mapped",[20698]],[[63932,63932],"mapped",[23534]],[[63933,63933],"mapped",[23615]],[[63934,63934],"mapped",[26009]],[[63935,63935],"mapped",[27138]],[[63936,63936],"mapped",[29134]],[[63937,63937],"mapped",[30274]],[[63938,63938],"mapped",[34044]],[[63939,63939],"mapped",[36988]],[[63940,63940],"mapped",[40845]],[[63941,63941],"mapped",[26248]],[[63942,63942],"mapped",[38446]],[[63943,63943],"mapped",[21129]],[[63944,63944],"mapped",[26491]],[[63945,63945],"mapped",[26611]],[[63946,63946],"mapped",[27969]],[[63947,63947],"mapped",[28316]],[[63948,63948],"mapped",[29705]],[[63949,63949],"mapped",[30041]],[[63950,63950],"mapped",[30827]],[[63951,63951],"mapped",[32016]],[[63952,63952],"mapped",[39006]],[[63953,63953],"mapped",[20845]],[[63954,63954],"mapped",[25134]],[[63955,63955],"mapped",[38520]],[[63956,63956],"mapped",[20523]],[[63957,63957],"mapped",[23833]],[[63958,63958],"mapped",[28138]],[[63959,63959],"mapped",[36650]],[[63960,63960],"mapped",[24459]],[[63961,63961],"mapped",[24900]],[[63962,63962],"mapped",[26647]],[[63963,63963],"mapped",[29575]],[[63964,63964],"mapped",[38534]],[[63965,63965],"mapped",[21033]],[[63966,63966],"mapped",[21519]],[[63967,63967],"mapped",[23653]],[[63968,63968],"mapped",[26131]],[[63969,63969],"mapped",[26446]],[[63970,63970],"mapped",[26792]],[[63971,63971],"mapped",[27877]],[[63972,63972],"mapped",[29702]],[[63973,63973],"mapped",[30178]],[[63974,63974],"mapped",[32633]],[[63975,63975],"mapped",[35023]],[[63976,63976],"mapped",[35041]],[[63977,63977],"mapped",[37324]],[[63978,63978],"mapped",[38626]],[[63979,63979],"mapped",[21311]],[[63980,63980],"mapped",[28346]],[[63981,63981],"mapped",[21533]],[[63982,63982],"mapped",[29136]],[[63983,63983],"mapped",[29848]],[[63984,63984],"mapped",[34298]],[[63985,63985],"mapped",[38563]],[[63986,63986],"mapped",[40023]],[[63987,63987],"mapped",[40607]],[[63988,63988],"mapped",[26519]],[[63989,63989],"mapped",[28107]],[[63990,63990],"mapped",[33256]],[[63991,63991],"mapped",[31435]],[[63992,63992],"mapped",[31520]],[[63993,63993],"mapped",[31890]],[[63994,63994],"mapped",[29376]],[[63995,63995],"mapped",[28825]],[[63996,63996],"mapped",[35672]],[[63997,63997],"mapped",[20160]],[[63998,63998],"mapped",[33590]],[[63999,63999],"mapped",[21050]],[[64000,64000],"mapped",[20999]],[[64001,64001],"mapped",[24230]],[[64002,64002],"mapped",[25299]],[[64003,64003],"mapped",[31958]],[[64004,64004],"mapped",[23429]],[[64005,64005],"mapped",[27934]],[[64006,64006],"mapped",[26292]],[[64007,64007],"mapped",[36667]],[[64008,64008],"mapped",[34892]],[[64009,64009],"mapped",[38477]],[[64010,64010],"mapped",[35211]],[[64011,64011],"mapped",[24275]],[[64012,64012],"mapped",[20800]],[[64013,64013],"mapped",[21952]],[[64014,64015],"valid"],[[64016,64016],"mapped",[22618]],[[64017,64017],"valid"],[[64018,64018],"mapped",[26228]],[[64019,64020],"valid"],[[64021,64021],"mapped",[20958]],[[64022,64022],"mapped",[29482]],[[64023,64023],"mapped",[30410]],[[64024,64024],"mapped",[31036]],[[64025,64025],"mapped",[31070]],[[64026,64026],"mapped",[31077]],[[64027,64027],"mapped",[31119]],[[64028,64028],"mapped",[38742]],[[64029,64029],"mapped",[31934]],[[64030,64030],"mapped",[32701]],[[64031,64031],"valid"],[[64032,64032],"mapped",[34322]],[[64033,64033],"valid"],[[64034,64034],"mapped",[35576]],[[64035,64036],"valid"],[[64037,64037],"mapped",[36920]],[[64038,64038],"mapped",[37117]],[[64039,64041],"valid"],[[64042,64042],"mapped",[39151]],[[64043,64043],"mapped",[39164]],[[64044,64044],"mapped",[39208]],[[64045,64045],"mapped",[40372]],[[64046,64046],"mapped",[37086]],[[64047,64047],"mapped",[38583]],[[64048,64048],"mapped",[20398]],[[64049,64049],"mapped",[20711]],[[64050,64050],"mapped",[20813]],[[64051,64051],"mapped",[21193]],[[64052,64052],"mapped",[21220]],[[64053,64053],"mapped",[21329]],[[64054,64054],"mapped",[21917]],[[64055,64055],"mapped",[22022]],[[64056,64056],"mapped",[22120]],[[64057,64057],"mapped",[22592]],[[64058,64058],"mapped",[22696]],[[64059,64059],"mapped",[23652]],[[64060,64060],"mapped",[23662]],[[64061,64061],"mapped",[24724]],[[64062,64062],"mapped",[24936]],[[64063,64063],"mapped",[24974]],[[64064,64064],"mapped",[25074]],[[64065,64065],"mapped",[25935]],[[64066,64066],"mapped",[26082]],[[64067,64067],"mapped",[26257]],[[64068,64068],"mapped",[26757]],[[64069,64069],"mapped",[28023]],[[64070,64070],"mapped",[28186]],[[64071,64071],"mapped",[28450]],[[64072,64072],"mapped",[29038]],[[64073,64073],"mapped",[29227]],[[64074,64074],"mapped",[29730]],[[64075,64075],"mapped",[30865]],[[64076,64076],"mapped",[31038]],[[64077,64077],"mapped",[31049]],[[64078,64078],"mapped",[31048]],[[64079,64079],"mapped",[31056]],[[64080,64080],"mapped",[31062]],[[64081,64081],"mapped",[31069]],[[64082,64082],"mapped",[31117]],[[64083,64083],"mapped",[31118]],[[64084,64084],"mapped",[31296]],[[64085,64085],"mapped",[31361]],[[64086,64086],"mapped",[31680]],[[64087,64087],"mapped",[32244]],[[64088,64088],"mapped",[32265]],[[64089,64089],"mapped",[32321]],[[64090,64090],"mapped",[32626]],[[64091,64091],"mapped",[32773]],[[64092,64092],"mapped",[33261]],[[64093,64094],"mapped",[33401]],[[64095,64095],"mapped",[33879]],[[64096,64096],"mapped",[35088]],[[64097,64097],"mapped",[35222]],[[64098,64098],"mapped",[35585]],[[64099,64099],"mapped",[35641]],[[64100,64100],"mapped",[36051]],[[64101,64101],"mapped",[36104]],[[64102,64102],"mapped",[36790]],[[64103,64103],"mapped",[36920]],[[64104,64104],"mapped",[38627]],[[64105,64105],"mapped",[38911]],[[64106,64106],"mapped",[38971]],[[64107,64107],"mapped",[24693]],[[64108,64108],"mapped",[148206]],[[64109,64109],"mapped",[33304]],[[64110,64111],"disallowed"],[[64112,64112],"mapped",[20006]],[[64113,64113],"mapped",[20917]],[[64114,64114],"mapped",[20840]],[[64115,64115],"mapped",[20352]],[[64116,64116],"mapped",[20805]],[[64117,64117],"mapped",[20864]],[[64118,64118],"mapped",[21191]],[[64119,64119],"mapped",[21242]],[[64120,64120],"mapped",[21917]],[[64121,64121],"mapped",[21845]],[[64122,64122],"mapped",[21913]],[[64123,64123],"mapped",[21986]],[[64124,64124],"mapped",[22618]],[[64125,64125],"mapped",[22707]],[[64126,64126],"mapped",[22852]],[[64127,64127],"mapped",[22868]],[[64128,64128],"mapped",[23138]],[[64129,64129],"mapped",[23336]],[[64130,64130],"mapped",[24274]],[[64131,64131],"mapped",[24281]],[[64132,64132],"mapped",[24425]],[[64133,64133],"mapped",[24493]],[[64134,64134],"mapped",[24792]],[[64135,64135],"mapped",[24910]],[[64136,64136],"mapped",[24840]],[[64137,64137],"mapped",[24974]],[[64138,64138],"mapped",[24928]],[[64139,64139],"mapped",[25074]],[[64140,64140],"mapped",[25140]],[[64141,64141],"mapped",[25540]],[[64142,64142],"mapped",[25628]],[[64143,64143],"mapped",[25682]],[[64144,64144],"mapped",[25942]],[[64145,64145],"mapped",[26228]],[[64146,64146],"mapped",[26391]],[[64147,64147],"mapped",[26395]],[[64148,64148],"mapped",[26454]],[[64149,64149],"mapped",[27513]],[[64150,64150],"mapped",[27578]],[[64151,64151],"mapped",[27969]],[[64152,64152],"mapped",[28379]],[[64153,64153],"mapped",[28363]],[[64154,64154],"mapped",[28450]],[[64155,64155],"mapped",[28702]],[[64156,64156],"mapped",[29038]],[[64157,64157],"mapped",[30631]],[[64158,64158],"mapped",[29237]],[[64159,64159],"mapped",[29359]],[[64160,64160],"mapped",[29482]],[[64161,64161],"mapped",[29809]],[[64162,64162],"mapped",[29958]],[[64163,64163],"mapped",[30011]],[[64164,64164],"mapped",[30237]],[[64165,64165],"mapped",[30239]],[[64166,64166],"mapped",[30410]],[[64167,64167],"mapped",[30427]],[[64168,64168],"mapped",[30452]],[[64169,64169],"mapped",[30538]],[[64170,64170],"mapped",[30528]],[[64171,64171],"mapped",[30924]],[[64172,64172],"mapped",[31409]],[[64173,64173],"mapped",[31680]],[[64174,64174],"mapped",[31867]],[[64175,64175],"mapped",[32091]],[[64176,64176],"mapped",[32244]],[[64177,64177],"mapped",[32574]],[[64178,64178],"mapped",[32773]],[[64179,64179],"mapped",[33618]],[[64180,64180],"mapped",[33775]],[[64181,64181],"mapped",[34681]],[[64182,64182],"mapped",[35137]],[[64183,64183],"mapped",[35206]],[[64184,64184],"mapped",[35222]],[[64185,64185],"mapped",[35519]],[[64186,64186],"mapped",[35576]],[[64187,64187],"mapped",[35531]],[[64188,64188],"mapped",[35585]],[[64189,64189],"mapped",[35582]],[[64190,64190],"mapped",[35565]],[[64191,64191],"mapped",[35641]],[[64192,64192],"mapped",[35722]],[[64193,64193],"mapped",[36104]],[[64194,64194],"mapped",[36664]],[[64195,64195],"mapped",[36978]],[[64196,64196],"mapped",[37273]],[[64197,64197],"mapped",[37494]],[[64198,64198],"mapped",[38524]],[[64199,64199],"mapped",[38627]],[[64200,64200],"mapped",[38742]],[[64201,64201],"mapped",[38875]],[[64202,64202],"mapped",[38911]],[[64203,64203],"mapped",[38923]],[[64204,64204],"mapped",[38971]],[[64205,64205],"mapped",[39698]],[[64206,64206],"mapped",[40860]],[[64207,64207],"mapped",[141386]],[[64208,64208],"mapped",[141380]],[[64209,64209],"mapped",[144341]],[[64210,64210],"mapped",[15261]],[[64211,64211],"mapped",[16408]],[[64212,64212],"mapped",[16441]],[[64213,64213],"mapped",[152137]],[[64214,64214],"mapped",[154832]],[[64215,64215],"mapped",[163539]],[[64216,64216],"mapped",[40771]],[[64217,64217],"mapped",[40846]],[[64218,64255],"disallowed"],[[64256,64256],"mapped",[102,102]],[[64257,64257],"mapped",[102,105]],[[64258,64258],"mapped",[102,108]],[[64259,64259],"mapped",[102,102,105]],[[64260,64260],"mapped",[102,102,108]],[[64261,64262],"mapped",[115,116]],[[64263,64274],"disallowed"],[[64275,64275],"mapped",[1396,1398]],[[64276,64276],"mapped",[1396,1381]],[[64277,64277],"mapped",[1396,1387]],[[64278,64278],"mapped",[1406,1398]],[[64279,64279],"mapped",[1396,1389]],[[64280,64284],"disallowed"],[[64285,64285],"mapped",[1497,1460]],[[64286,64286],"valid"],[[64287,64287],"mapped",[1522,1463]],[[64288,64288],"mapped",[1506]],[[64289,64289],"mapped",[1488]],[[64290,64290],"mapped",[1491]],[[64291,64291],"mapped",[1492]],[[64292,64292],"mapped",[1499]],[[64293,64293],"mapped",[1500]],[[64294,64294],"mapped",[1501]],[[64295,64295],"mapped",[1512]],[[64296,64296],"mapped",[1514]],[[64297,64297],"disallowed_STD3_mapped",[43]],[[64298,64298],"mapped",[1513,1473]],[[64299,64299],"mapped",[1513,1474]],[[64300,64300],"mapped",[1513,1468,1473]],[[64301,64301],"mapped",[1513,1468,1474]],[[64302,64302],"mapped",[1488,1463]],[[64303,64303],"mapped",[1488,1464]],[[64304,64304],"mapped",[1488,1468]],[[64305,64305],"mapped",[1489,1468]],[[64306,64306],"mapped",[1490,1468]],[[64307,64307],"mapped",[1491,1468]],[[64308,64308],"mapped",[1492,1468]],[[64309,64309],"mapped",[1493,1468]],[[64310,64310],"mapped",[1494,1468]],[[64311,64311],"disallowed"],[[64312,64312],"mapped",[1496,1468]],[[64313,64313],"mapped",[1497,1468]],[[64314,64314],"mapped",[1498,1468]],[[64315,64315],"mapped",[1499,1468]],[[64316,64316],"mapped",[1500,1468]],[[64317,64317],"disallowed"],[[64318,64318],"mapped",[1502,1468]],[[64319,64319],"disallowed"],[[64320,64320],"mapped",[1504,1468]],[[64321,64321],"mapped",[1505,1468]],[[64322,64322],"disallowed"],[[64323,64323],"mapped",[1507,1468]],[[64324,64324],"mapped",[1508,1468]],[[64325,64325],"disallowed"],[[64326,64326],"mapped",[1510,1468]],[[64327,64327],"mapped",[1511,1468]],[[64328,64328],"mapped",[1512,1468]],[[64329,64329],"mapped",[1513,1468]],[[64330,64330],"mapped",[1514,1468]],[[64331,64331],"mapped",[1493,1465]],[[64332,64332],"mapped",[1489,1471]],[[64333,64333],"mapped",[1499,1471]],[[64334,64334],"mapped",[1508,1471]],[[64335,64335],"mapped",[1488,1500]],[[64336,64337],"mapped",[1649]],[[64338,64341],"mapped",[1659]],[[64342,64345],"mapped",[1662]],[[64346,64349],"mapped",[1664]],[[64350,64353],"mapped",[1658]],[[64354,64357],"mapped",[1663]],[[64358,64361],"mapped",[1657]],[[64362,64365],"mapped",[1700]],[[64366,64369],"mapped",[1702]],[[64370,64373],"mapped",[1668]],[[64374,64377],"mapped",[1667]],[[64378,64381],"mapped",[1670]],[[64382,64385],"mapped",[1671]],[[64386,64387],"mapped",[1677]],[[64388,64389],"mapped",[1676]],[[64390,64391],"mapped",[1678]],[[64392,64393],"mapped",[1672]],[[64394,64395],"mapped",[1688]],[[64396,64397],"mapped",[1681]],[[64398,64401],"mapped",[1705]],[[64402,64405],"mapped",[1711]],[[64406,64409],"mapped",[1715]],[[64410,64413],"mapped",[1713]],[[64414,64415],"mapped",[1722]],[[64416,64419],"mapped",[1723]],[[64420,64421],"mapped",[1728]],[[64422,64425],"mapped",[1729]],[[64426,64429],"mapped",[1726]],[[64430,64431],"mapped",[1746]],[[64432,64433],"mapped",[1747]],[[64434,64449],"valid",[],"NV8"],[[64450,64466],"disallowed"],[[64467,64470],"mapped",[1709]],[[64471,64472],"mapped",[1735]],[[64473,64474],"mapped",[1734]],[[64475,64476],"mapped",[1736]],[[64477,64477],"mapped",[1735,1652]],[[64478,64479],"mapped",[1739]],[[64480,64481],"mapped",[1733]],[[64482,64483],"mapped",[1737]],[[64484,64487],"mapped",[1744]],[[64488,64489],"mapped",[1609]],[[64490,64491],"mapped",[1574,1575]],[[64492,64493],"mapped",[1574,1749]],[[64494,64495],"mapped",[1574,1608]],[[64496,64497],"mapped",[1574,1735]],[[64498,64499],"mapped",[1574,1734]],[[64500,64501],"mapped",[1574,1736]],[[64502,64504],"mapped",[1574,1744]],[[64505,64507],"mapped",[1574,1609]],[[64508,64511],"mapped",[1740]],[[64512,64512],"mapped",[1574,1580]],[[64513,64513],"mapped",[1574,1581]],[[64514,64514],"mapped",[1574,1605]],[[64515,64515],"mapped",[1574,1609]],[[64516,64516],"mapped",[1574,1610]],[[64517,64517],"mapped",[1576,1580]],[[64518,64518],"mapped",[1576,1581]],[[64519,64519],"mapped",[1576,1582]],[[64520,64520],"mapped",[1576,1605]],[[64521,64521],"mapped",[1576,1609]],[[64522,64522],"mapped",[1576,1610]],[[64523,64523],"mapped",[1578,1580]],[[64524,64524],"mapped",[1578,1581]],[[64525,64525],"mapped",[1578,1582]],[[64526,64526],"mapped",[1578,1605]],[[64527,64527],"mapped",[1578,1609]],[[64528,64528],"mapped",[1578,1610]],[[64529,64529],"mapped",[1579,1580]],[[64530,64530],"mapped",[1579,1605]],[[64531,64531],"mapped",[1579,1609]],[[64532,64532],"mapped",[1579,1610]],[[64533,64533],"mapped",[1580,1581]],[[64534,64534],"mapped",[1580,1605]],[[64535,64535],"mapped",[1581,1580]],[[64536,64536],"mapped",[1581,1605]],[[64537,64537],"mapped",[1582,1580]],[[64538,64538],"mapped",[1582,1581]],[[64539,64539],"mapped",[1582,1605]],[[64540,64540],"mapped",[1587,1580]],[[64541,64541],"mapped",[1587,1581]],[[64542,64542],"mapped",[1587,1582]],[[64543,64543],"mapped",[1587,1605]],[[64544,64544],"mapped",[1589,1581]],[[64545,64545],"mapped",[1589,1605]],[[64546,64546],"mapped",[1590,1580]],[[64547,64547],"mapped",[1590,1581]],[[64548,64548],"mapped",[1590,1582]],[[64549,64549],"mapped",[1590,1605]],[[64550,64550],"mapped",[1591,1581]],[[64551,64551],"mapped",[1591,1605]],[[64552,64552],"mapped",[1592,1605]],[[64553,64553],"mapped",[1593,1580]],[[64554,64554],"mapped",[1593,1605]],[[64555,64555],"mapped",[1594,1580]],[[64556,64556],"mapped",[1594,1605]],[[64557,64557],"mapped",[1601,1580]],[[64558,64558],"mapped",[1601,1581]],[[64559,64559],"mapped",[1601,1582]],[[64560,64560],"mapped",[1601,1605]],[[64561,64561],"mapped",[1601,1609]],[[64562,64562],"mapped",[1601,1610]],[[64563,64563],"mapped",[1602,1581]],[[64564,64564],"mapped",[1602,1605]],[[64565,64565],"mapped",[1602,1609]],[[64566,64566],"mapped",[1602,1610]],[[64567,64567],"mapped",[1603,1575]],[[64568,64568],"mapped",[1603,1580]],[[64569,64569],"mapped",[1603,1581]],[[64570,64570],"mapped",[1603,1582]],[[64571,64571],"mapped",[1603,1604]],[[64572,64572],"mapped",[1603,1605]],[[64573,64573],"mapped",[1603,1609]],[[64574,64574],"mapped",[1603,1610]],[[64575,64575],"mapped",[1604,1580]],[[64576,64576],"mapped",[1604,1581]],[[64577,64577],"mapped",[1604,1582]],[[64578,64578],"mapped",[1604,1605]],[[64579,64579],"mapped",[1604,1609]],[[64580,64580],"mapped",[1604,1610]],[[64581,64581],"mapped",[1605,1580]],[[64582,64582],"mapped",[1605,1581]],[[64583,64583],"mapped",[1605,1582]],[[64584,64584],"mapped",[1605,1605]],[[64585,64585],"mapped",[1605,1609]],[[64586,64586],"mapped",[1605,1610]],[[64587,64587],"mapped",[1606,1580]],[[64588,64588],"mapped",[1606,1581]],[[64589,64589],"mapped",[1606,1582]],[[64590,64590],"mapped",[1606,1605]],[[64591,64591],"mapped",[1606,1609]],[[64592,64592],"mapped",[1606,1610]],[[64593,64593],"mapped",[1607,1580]],[[64594,64594],"mapped",[1607,1605]],[[64595,64595],"mapped",[1607,1609]],[[64596,64596],"mapped",[1607,1610]],[[64597,64597],"mapped",[1610,1580]],[[64598,64598],"mapped",[1610,1581]],[[64599,64599],"mapped",[1610,1582]],[[64600,64600],"mapped",[1610,1605]],[[64601,64601],"mapped",[1610,1609]],[[64602,64602],"mapped",[1610,1610]],[[64603,64603],"mapped",[1584,1648]],[[64604,64604],"mapped",[1585,1648]],[[64605,64605],"mapped",[1609,1648]],[[64606,64606],"disallowed_STD3_mapped",[32,1612,1617]],[[64607,64607],"disallowed_STD3_mapped",[32,1613,1617]],[[64608,64608],"disallowed_STD3_mapped",[32,1614,1617]],[[64609,64609],"disallowed_STD3_mapped",[32,1615,1617]],[[64610,64610],"disallowed_STD3_mapped",[32,1616,1617]],[[64611,64611],"disallowed_STD3_mapped",[32,1617,1648]],[[64612,64612],"mapped",[1574,1585]],[[64613,64613],"mapped",[1574,1586]],[[64614,64614],"mapped",[1574,1605]],[[64615,64615],"mapped",[1574,1606]],[[64616,64616],"mapped",[1574,1609]],[[64617,64617],"mapped",[1574,1610]],[[64618,64618],"mapped",[1576,1585]],[[64619,64619],"mapped",[1576,1586]],[[64620,64620],"mapped",[1576,1605]],[[64621,64621],"mapped",[1576,1606]],[[64622,64622],"mapped",[1576,1609]],[[64623,64623],"mapped",[1576,1610]],[[64624,64624],"mapped",[1578,1585]],[[64625,64625],"mapped",[1578,1586]],[[64626,64626],"mapped",[1578,1605]],[[64627,64627],"mapped",[1578,1606]],[[64628,64628],"mapped",[1578,1609]],[[64629,64629],"mapped",[1578,1610]],[[64630,64630],"mapped",[1579,1585]],[[64631,64631],"mapped",[1579,1586]],[[64632,64632],"mapped",[1579,1605]],[[64633,64633],"mapped",[1579,1606]],[[64634,64634],"mapped",[1579,1609]],[[64635,64635],"mapped",[1579,1610]],[[64636,64636],"mapped",[1601,1609]],[[64637,64637],"mapped",[1601,1610]],[[64638,64638],"mapped",[1602,1609]],[[64639,64639],"mapped",[1602,1610]],[[64640,64640],"mapped",[1603,1575]],[[64641,64641],"mapped",[1603,1604]],[[64642,64642],"mapped",[1603,1605]],[[64643,64643],"mapped",[1603,1609]],[[64644,64644],"mapped",[1603,1610]],[[64645,64645],"mapped",[1604,1605]],[[64646,64646],"mapped",[1604,1609]],[[64647,64647],"mapped",[1604,1610]],[[64648,64648],"mapped",[1605,1575]],[[64649,64649],"mapped",[1605,1605]],[[64650,64650],"mapped",[1606,1585]],[[64651,64651],"mapped",[1606,1586]],[[64652,64652],"mapped",[1606,1605]],[[64653,64653],"mapped",[1606,1606]],[[64654,64654],"mapped",[1606,1609]],[[64655,64655],"mapped",[1606,1610]],[[64656,64656],"mapped",[1609,1648]],[[64657,64657],"mapped",[1610,1585]],[[64658,64658],"mapped",[1610,1586]],[[64659,64659],"mapped",[1610,1605]],[[64660,64660],"mapped",[1610,1606]],[[64661,64661],"mapped",[1610,1609]],[[64662,64662],"mapped",[1610,1610]],[[64663,64663],"mapped",[1574,1580]],[[64664,64664],"mapped",[1574,1581]],[[64665,64665],"mapped",[1574,1582]],[[64666,64666],"mapped",[1574,1605]],[[64667,64667],"mapped",[1574,1607]],[[64668,64668],"mapped",[1576,1580]],[[64669,64669],"mapped",[1576,1581]],[[64670,64670],"mapped",[1576,1582]],[[64671,64671],"mapped",[1576,1605]],[[64672,64672],"mapped",[1576,1607]],[[64673,64673],"mapped",[1578,1580]],[[64674,64674],"mapped",[1578,1581]],[[64675,64675],"mapped",[1578,1582]],[[64676,64676],"mapped",[1578,1605]],[[64677,64677],"mapped",[1578,1607]],[[64678,64678],"mapped",[1579,1605]],[[64679,64679],"mapped",[1580,1581]],[[64680,64680],"mapped",[1580,1605]],[[64681,64681],"mapped",[1581,1580]],[[64682,64682],"mapped",[1581,1605]],[[64683,64683],"mapped",[1582,1580]],[[64684,64684],"mapped",[1582,1605]],[[64685,64685],"mapped",[1587,1580]],[[64686,64686],"mapped",[1587,1581]],[[64687,64687],"mapped",[1587,1582]],[[64688,64688],"mapped",[1587,1605]],[[64689,64689],"mapped",[1589,1581]],[[64690,64690],"mapped",[1589,1582]],[[64691,64691],"mapped",[1589,1605]],[[64692,64692],"mapped",[1590,1580]],[[64693,64693],"mapped",[1590,1581]],[[64694,64694],"mapped",[1590,1582]],[[64695,64695],"mapped",[1590,1605]],[[64696,64696],"mapped",[1591,1581]],[[64697,64697],"mapped",[1592,1605]],[[64698,64698],"mapped",[1593,1580]],[[64699,64699],"mapped",[1593,1605]],[[64700,64700],"mapped",[1594,1580]],[[64701,64701],"mapped",[1594,1605]],[[64702,64702],"mapped",[1601,1580]],[[64703,64703],"mapped",[1601,1581]],[[64704,64704],"mapped",[1601,1582]],[[64705,64705],"mapped",[1601,1605]],[[64706,64706],"mapped",[1602,1581]],[[64707,64707],"mapped",[1602,1605]],[[64708,64708],"mapped",[1603,1580]],[[64709,64709],"mapped",[1603,1581]],[[64710,64710],"mapped",[1603,1582]],[[64711,64711],"mapped",[1603,1604]],[[64712,64712],"mapped",[1603,1605]],[[64713,64713],"mapped",[1604,1580]],[[64714,64714],"mapped",[1604,1581]],[[64715,64715],"mapped",[1604,1582]],[[64716,64716],"mapped",[1604,1605]],[[64717,64717],"mapped",[1604,1607]],[[64718,64718],"mapped",[1605,1580]],[[64719,64719],"mapped",[1605,1581]],[[64720,64720],"mapped",[1605,1582]],[[64721,64721],"mapped",[1605,1605]],[[64722,64722],"mapped",[1606,1580]],[[64723,64723],"mapped",[1606,1581]],[[64724,64724],"mapped",[1606,1582]],[[64725,64725],"mapped",[1606,1605]],[[64726,64726],"mapped",[1606,1607]],[[64727,64727],"mapped",[1607,1580]],[[64728,64728],"mapped",[1607,1605]],[[64729,64729],"mapped",[1607,1648]],[[64730,64730],"mapped",[1610,1580]],[[64731,64731],"mapped",[1610,1581]],[[64732,64732],"mapped",[1610,1582]],[[64733,64733],"mapped",[1610,1605]],[[64734,64734],"mapped",[1610,1607]],[[64735,64735],"mapped",[1574,1605]],[[64736,64736],"mapped",[1574,1607]],[[64737,64737],"mapped",[1576,1605]],[[64738,64738],"mapped",[1576,1607]],[[64739,64739],"mapped",[1578,1605]],[[64740,64740],"mapped",[1578,1607]],[[64741,64741],"mapped",[1579,1605]],[[64742,64742],"mapped",[1579,1607]],[[64743,64743],"mapped",[1587,1605]],[[64744,64744],"mapped",[1587,1607]],[[64745,64745],"mapped",[1588,1605]],[[64746,64746],"mapped",[1588,1607]],[[64747,64747],"mapped",[1603,1604]],[[64748,64748],"mapped",[1603,1605]],[[64749,64749],"mapped",[1604,1605]],[[64750,64750],"mapped",[1606,1605]],[[64751,64751],"mapped",[1606,1607]],[[64752,64752],"mapped",[1610,1605]],[[64753,64753],"mapped",[1610,1607]],[[64754,64754],"mapped",[1600,1614,1617]],[[64755,64755],"mapped",[1600,1615,1617]],[[64756,64756],"mapped",[1600,1616,1617]],[[64757,64757],"mapped",[1591,1609]],[[64758,64758],"mapped",[1591,1610]],[[64759,64759],"mapped",[1593,1609]],[[64760,64760],"mapped",[1593,1610]],[[64761,64761],"mapped",[1594,1609]],[[64762,64762],"mapped",[1594,1610]],[[64763,64763],"mapped",[1587,1609]],[[64764,64764],"mapped",[1587,1610]],[[64765,64765],"mapped",[1588,1609]],[[64766,64766],"mapped",[1588,1610]],[[64767,64767],"mapped",[1581,1609]],[[64768,64768],"mapped",[1581,1610]],[[64769,64769],"mapped",[1580,1609]],[[64770,64770],"mapped",[1580,1610]],[[64771,64771],"mapped",[1582,1609]],[[64772,64772],"mapped",[1582,1610]],[[64773,64773],"mapped",[1589,1609]],[[64774,64774],"mapped",[1589,1610]],[[64775,64775],"mapped",[1590,1609]],[[64776,64776],"mapped",[1590,1610]],[[64777,64777],"mapped",[1588,1580]],[[64778,64778],"mapped",[1588,1581]],[[64779,64779],"mapped",[1588,1582]],[[64780,64780],"mapped",[1588,1605]],[[64781,64781],"mapped",[1588,1585]],[[64782,64782],"mapped",[1587,1585]],[[64783,64783],"mapped",[1589,1585]],[[64784,64784],"mapped",[1590,1585]],[[64785,64785],"mapped",[1591,1609]],[[64786,64786],"mapped",[1591,1610]],[[64787,64787],"mapped",[1593,1609]],[[64788,64788],"mapped",[1593,1610]],[[64789,64789],"mapped",[1594,1609]],[[64790,64790],"mapped",[1594,1610]],[[64791,64791],"mapped",[1587,1609]],[[64792,64792],"mapped",[1587,1610]],[[64793,64793],"mapped",[1588,1609]],[[64794,64794],"mapped",[1588,1610]],[[64795,64795],"mapped",[1581,1609]],[[64796,64796],"mapped",[1581,1610]],[[64797,64797],"mapped",[1580,1609]],[[64798,64798],"mapped",[1580,1610]],[[64799,64799],"mapped",[1582,1609]],[[64800,64800],"mapped",[1582,1610]],[[64801,64801],"mapped",[1589,1609]],[[64802,64802],"mapped",[1589,1610]],[[64803,64803],"mapped",[1590,1609]],[[64804,64804],"mapped",[1590,1610]],[[64805,64805],"mapped",[1588,1580]],[[64806,64806],"mapped",[1588,1581]],[[64807,64807],"mapped",[1588,1582]],[[64808,64808],"mapped",[1588,1605]],[[64809,64809],"mapped",[1588,1585]],[[64810,64810],"mapped",[1587,1585]],[[64811,64811],"mapped",[1589,1585]],[[64812,64812],"mapped",[1590,1585]],[[64813,64813],"mapped",[1588,1580]],[[64814,64814],"mapped",[1588,1581]],[[64815,64815],"mapped",[1588,1582]],[[64816,64816],"mapped",[1588,1605]],[[64817,64817],"mapped",[1587,1607]],[[64818,64818],"mapped",[1588,1607]],[[64819,64819],"mapped",[1591,1605]],[[64820,64820],"mapped",[1587,1580]],[[64821,64821],"mapped",[1587,1581]],[[64822,64822],"mapped",[1587,1582]],[[64823,64823],"mapped",[1588,1580]],[[64824,64824],"mapped",[1588,1581]],[[64825,64825],"mapped",[1588,1582]],[[64826,64826],"mapped",[1591,1605]],[[64827,64827],"mapped",[1592,1605]],[[64828,64829],"mapped",[1575,1611]],[[64830,64831],"valid",[],"NV8"],[[64832,64847],"disallowed"],[[64848,64848],"mapped",[1578,1580,1605]],[[64849,64850],"mapped",[1578,1581,1580]],[[64851,64851],"mapped",[1578,1581,1605]],[[64852,64852],"mapped",[1578,1582,1605]],[[64853,64853],"mapped",[1578,1605,1580]],[[64854,64854],"mapped",[1578,1605,1581]],[[64855,64855],"mapped",[1578,1605,1582]],[[64856,64857],"mapped",[1580,1605,1581]],[[64858,64858],"mapped",[1581,1605,1610]],[[64859,64859],"mapped",[1581,1605,1609]],[[64860,64860],"mapped",[1587,1581,1580]],[[64861,64861],"mapped",[1587,1580,1581]],[[64862,64862],"mapped",[1587,1580,1609]],[[64863,64864],"mapped",[1587,1605,1581]],[[64865,64865],"mapped",[1587,1605,1580]],[[64866,64867],"mapped",[1587,1605,1605]],[[64868,64869],"mapped",[1589,1581,1581]],[[64870,64870],"mapped",[1589,1605,1605]],[[64871,64872],"mapped",[1588,1581,1605]],[[64873,64873],"mapped",[1588,1580,1610]],[[64874,64875],"mapped",[1588,1605,1582]],[[64876,64877],"mapped",[1588,1605,1605]],[[64878,64878],"mapped",[1590,1581,1609]],[[64879,64880],"mapped",[1590,1582,1605]],[[64881,64882],"mapped",[1591,1605,1581]],[[64883,64883],"mapped",[1591,1605,1605]],[[64884,64884],"mapped",[1591,1605,1610]],[[64885,64885],"mapped",[1593,1580,1605]],[[64886,64887],"mapped",[1593,1605,1605]],[[64888,64888],"mapped",[1593,1605,1609]],[[64889,64889],"mapped",[1594,1605,1605]],[[64890,64890],"mapped",[1594,1605,1610]],[[64891,64891],"mapped",[1594,1605,1609]],[[64892,64893],"mapped",[1601,1582,1605]],[[64894,64894],"mapped",[1602,1605,1581]],[[64895,64895],"mapped",[1602,1605,1605]],[[64896,64896],"mapped",[1604,1581,1605]],[[64897,64897],"mapped",[1604,1581,1610]],[[64898,64898],"mapped",[1604,1581,1609]],[[64899,64900],"mapped",[1604,1580,1580]],[[64901,64902],"mapped",[1604,1582,1605]],[[64903,64904],"mapped",[1604,1605,1581]],[[64905,64905],"mapped",[1605,1581,1580]],[[64906,64906],"mapped",[1605,1581,1605]],[[64907,64907],"mapped",[1605,1581,1610]],[[64908,64908],"mapped",[1605,1580,1581]],[[64909,64909],"mapped",[1605,1580,1605]],[[64910,64910],"mapped",[1605,1582,1580]],[[64911,64911],"mapped",[1605,1582,1605]],[[64912,64913],"disallowed"],[[64914,64914],"mapped",[1605,1580,1582]],[[64915,64915],"mapped",[1607,1605,1580]],[[64916,64916],"mapped",[1607,1605,1605]],[[64917,64917],"mapped",[1606,1581,1605]],[[64918,64918],"mapped",[1606,1581,1609]],[[64919,64920],"mapped",[1606,1580,1605]],[[64921,64921],"mapped",[1606,1580,1609]],[[64922,64922],"mapped",[1606,1605,1610]],[[64923,64923],"mapped",[1606,1605,1609]],[[64924,64925],"mapped",[1610,1605,1605]],[[64926,64926],"mapped",[1576,1582,1610]],[[64927,64927],"mapped",[1578,1580,1610]],[[64928,64928],"mapped",[1578,1580,1609]],[[64929,64929],"mapped",[1578,1582,1610]],[[64930,64930],"mapped",[1578,1582,1609]],[[64931,64931],"mapped",[1578,1605,1610]],[[64932,64932],"mapped",[1578,1605,1609]],[[64933,64933],"mapped",[1580,1605,1610]],[[64934,64934],"mapped",[1580,1581,1609]],[[64935,64935],"mapped",[1580,1605,1609]],[[64936,64936],"mapped",[1587,1582,1609]],[[64937,64937],"mapped",[1589,1581,1610]],[[64938,64938],"mapped",[1588,1581,1610]],[[64939,64939],"mapped",[1590,1581,1610]],[[64940,64940],"mapped",[1604,1580,1610]],[[64941,64941],"mapped",[1604,1605,1610]],[[64942,64942],"mapped",[1610,1581,1610]],[[64943,64943],"mapped",[1610,1580,1610]],[[64944,64944],"mapped",[1610,1605,1610]],[[64945,64945],"mapped",[1605,1605,1610]],[[64946,64946],"mapped",[1602,1605,1610]],[[64947,64947],"mapped",[1606,1581,1610]],[[64948,64948],"mapped",[1602,1605,1581]],[[64949,64949],"mapped",[1604,1581,1605]],[[64950,64950],"mapped",[1593,1605,1610]],[[64951,64951],"mapped",[1603,1605,1610]],[[64952,64952],"mapped",[1606,1580,1581]],[[64953,64953],"mapped",[1605,1582,1610]],[[64954,64954],"mapped",[1604,1580,1605]],[[64955,64955],"mapped",[1603,1605,1605]],[[64956,64956],"mapped",[1604,1580,1605]],[[64957,64957],"mapped",[1606,1580,1581]],[[64958,64958],"mapped",[1580,1581,1610]],[[64959,64959],"mapped",[1581,1580,1610]],[[64960,64960],"mapped",[1605,1580,1610]],[[64961,64961],"mapped",[1601,1605,1610]],[[64962,64962],"mapped",[1576,1581,1610]],[[64963,64963],"mapped",[1603,1605,1605]],[[64964,64964],"mapped",[1593,1580,1605]],[[64965,64965],"mapped",[1589,1605,1605]],[[64966,64966],"mapped",[1587,1582,1610]],[[64967,64967],"mapped",[1606,1580,1610]],[[64968,64975],"disallowed"],[[64976,65007],"disallowed"],[[65008,65008],"mapped",[1589,1604,1746]],[[65009,65009],"mapped",[1602,1604,1746]],[[65010,65010],"mapped",[1575,1604,1604,1607]],[[65011,65011],"mapped",[1575,1603,1576,1585]],[[65012,65012],"mapped",[1605,1581,1605,1583]],[[65013,65013],"mapped",[1589,1604,1593,1605]],[[65014,65014],"mapped",[1585,1587,1608,1604]],[[65015,65015],"mapped",[1593,1604,1610,1607]],[[65016,65016],"mapped",[1608,1587,1604,1605]],[[65017,65017],"mapped",[1589,1604,1609]],[[65018,65018],"disallowed_STD3_mapped",[1589,1604,1609,32,1575,1604,1604,1607,32,1593,1604,1610,1607,32,1608,1587,1604,1605]],[[65019,65019],"disallowed_STD3_mapped",[1580,1604,32,1580,1604,1575,1604,1607]],[[65020,65020],"mapped",[1585,1740,1575,1604]],[[65021,65021],"valid",[],"NV8"],[[65022,65023],"disallowed"],[[65024,65039],"ignored"],[[65040,65040],"disallowed_STD3_mapped",[44]],[[65041,65041],"mapped",[12289]],[[65042,65042],"disallowed"],[[65043,65043],"disallowed_STD3_mapped",[58]],[[65044,65044],"disallowed_STD3_mapped",[59]],[[65045,65045],"disallowed_STD3_mapped",[33]],[[65046,65046],"disallowed_STD3_mapped",[63]],[[65047,65047],"mapped",[12310]],[[65048,65048],"mapped",[12311]],[[65049,65049],"disallowed"],[[65050,65055],"disallowed"],[[65056,65059],"valid"],[[65060,65062],"valid"],[[65063,65069],"valid"],[[65070,65071],"valid"],[[65072,65072],"disallowed"],[[65073,65073],"mapped",[8212]],[[65074,65074],"mapped",[8211]],[[65075,65076],"disallowed_STD3_mapped",[95]],[[65077,65077],"disallowed_STD3_mapped",[40]],[[65078,65078],"disallowed_STD3_mapped",[41]],[[65079,65079],"disallowed_STD3_mapped",[123]],[[65080,65080],"disallowed_STD3_mapped",[125]],[[65081,65081],"mapped",[12308]],[[65082,65082],"mapped",[12309]],[[65083,65083],"mapped",[12304]],[[65084,65084],"mapped",[12305]],[[65085,65085],"mapped",[12298]],[[65086,65086],"mapped",[12299]],[[65087,65087],"mapped",[12296]],[[65088,65088],"mapped",[12297]],[[65089,65089],"mapped",[12300]],[[65090,65090],"mapped",[12301]],[[65091,65091],"mapped",[12302]],[[65092,65092],"mapped",[12303]],[[65093,65094],"valid",[],"NV8"],[[65095,65095],"disallowed_STD3_mapped",[91]],[[65096,65096],"disallowed_STD3_mapped",[93]],[[65097,65100],"disallowed_STD3_mapped",[32,773]],[[65101,65103],"disallowed_STD3_mapped",[95]],[[65104,65104],"disallowed_STD3_mapped",[44]],[[65105,65105],"mapped",[12289]],[[65106,65106],"disallowed"],[[65107,65107],"disallowed"],[[65108,65108],"disallowed_STD3_mapped",[59]],[[65109,65109],"disallowed_STD3_mapped",[58]],[[65110,65110],"disallowed_STD3_mapped",[63]],[[65111,65111],"disallowed_STD3_mapped",[33]],[[65112,65112],"mapped",[8212]],[[65113,65113],"disallowed_STD3_mapped",[40]],[[65114,65114],"disallowed_STD3_mapped",[41]],[[65115,65115],"disallowed_STD3_mapped",[123]],[[65116,65116],"disallowed_STD3_mapped",[125]],[[65117,65117],"mapped",[12308]],[[65118,65118],"mapped",[12309]],[[65119,65119],"disallowed_STD3_mapped",[35]],[[65120,65120],"disallowed_STD3_mapped",[38]],[[65121,65121],"disallowed_STD3_mapped",[42]],[[65122,65122],"disallowed_STD3_mapped",[43]],[[65123,65123],"mapped",[45]],[[65124,65124],"disallowed_STD3_mapped",[60]],[[65125,65125],"disallowed_STD3_mapped",[62]],[[65126,65126],"disallowed_STD3_mapped",[61]],[[65127,65127],"disallowed"],[[65128,65128],"disallowed_STD3_mapped",[92]],[[65129,65129],"disallowed_STD3_mapped",[36]],[[65130,65130],"disallowed_STD3_mapped",[37]],[[65131,65131],"disallowed_STD3_mapped",[64]],[[65132,65135],"disallowed"],[[65136,65136],"disallowed_STD3_mapped",[32,1611]],[[65137,65137],"mapped",[1600,1611]],[[65138,65138],"disallowed_STD3_mapped",[32,1612]],[[65139,65139],"valid"],[[65140,65140],"disallowed_STD3_mapped",[32,1613]],[[65141,65141],"disallowed"],[[65142,65142],"disallowed_STD3_mapped",[32,1614]],[[65143,65143],"mapped",[1600,1614]],[[65144,65144],"disallowed_STD3_mapped",[32,1615]],[[65145,65145],"mapped",[1600,1615]],[[65146,65146],"disallowed_STD3_mapped",[32,1616]],[[65147,65147],"mapped",[1600,1616]],[[65148,65148],"disallowed_STD3_mapped",[32,1617]],[[65149,65149],"mapped",[1600,1617]],[[65150,65150],"disallowed_STD3_mapped",[32,1618]],[[65151,65151],"mapped",[1600,1618]],[[65152,65152],"mapped",[1569]],[[65153,65154],"mapped",[1570]],[[65155,65156],"mapped",[1571]],[[65157,65158],"mapped",[1572]],[[65159,65160],"mapped",[1573]],[[65161,65164],"mapped",[1574]],[[65165,65166],"mapped",[1575]],[[65167,65170],"mapped",[1576]],[[65171,65172],"mapped",[1577]],[[65173,65176],"mapped",[1578]],[[65177,65180],"mapped",[1579]],[[65181,65184],"mapped",[1580]],[[65185,65188],"mapped",[1581]],[[65189,65192],"mapped",[1582]],[[65193,65194],"mapped",[1583]],[[65195,65196],"mapped",[1584]],[[65197,65198],"mapped",[1585]],[[65199,65200],"mapped",[1586]],[[65201,65204],"mapped",[1587]],[[65205,65208],"mapped",[1588]],[[65209,65212],"mapped",[1589]],[[65213,65216],"mapped",[1590]],[[65217,65220],"mapped",[1591]],[[65221,65224],"mapped",[1592]],[[65225,65228],"mapped",[1593]],[[65229,65232],"mapped",[1594]],[[65233,65236],"mapped",[1601]],[[65237,65240],"mapped",[1602]],[[65241,65244],"mapped",[1603]],[[65245,65248],"mapped",[1604]],[[65249,65252],"mapped",[1605]],[[65253,65256],"mapped",[1606]],[[65257,65260],"mapped",[1607]],[[65261,65262],"mapped",[1608]],[[65263,65264],"mapped",[1609]],[[65265,65268],"mapped",[1610]],[[65269,65270],"mapped",[1604,1570]],[[65271,65272],"mapped",[1604,1571]],[[65273,65274],"mapped",[1604,1573]],[[65275,65276],"mapped",[1604,1575]],[[65277,65278],"disallowed"],[[65279,65279],"ignored"],[[65280,65280],"disallowed"],[[65281,65281],"disallowed_STD3_mapped",[33]],[[65282,65282],"disallowed_STD3_mapped",[34]],[[65283,65283],"disallowed_STD3_mapped",[35]],[[65284,65284],"disallowed_STD3_mapped",[36]],[[65285,65285],"disallowed_STD3_mapped",[37]],[[65286,65286],"disallowed_STD3_mapped",[38]],[[65287,65287],"disallowed_STD3_mapped",[39]],[[65288,65288],"disallowed_STD3_mapped",[40]],[[65289,65289],"disallowed_STD3_mapped",[41]],[[65290,65290],"disallowed_STD3_mapped",[42]],[[65291,65291],"disallowed_STD3_mapped",[43]],[[65292,65292],"disallowed_STD3_mapped",[44]],[[65293,65293],"mapped",[45]],[[65294,65294],"mapped",[46]],[[65295,65295],"disallowed_STD3_mapped",[47]],[[65296,65296],"mapped",[48]],[[65297,65297],"mapped",[49]],[[65298,65298],"mapped",[50]],[[65299,65299],"mapped",[51]],[[65300,65300],"mapped",[52]],[[65301,65301],"mapped",[53]],[[65302,65302],"mapped",[54]],[[65303,65303],"mapped",[55]],[[65304,65304],"mapped",[56]],[[65305,65305],"mapped",[57]],[[65306,65306],"disallowed_STD3_mapped",[58]],[[65307,65307],"disallowed_STD3_mapped",[59]],[[65308,65308],"disallowed_STD3_mapped",[60]],[[65309,65309],"disallowed_STD3_mapped",[61]],[[65310,65310],"disallowed_STD3_mapped",[62]],[[65311,65311],"disallowed_STD3_mapped",[63]],[[65312,65312],"disallowed_STD3_mapped",[64]],[[65313,65313],"mapped",[97]],[[65314,65314],"mapped",[98]],[[65315,65315],"mapped",[99]],[[65316,65316],"mapped",[100]],[[65317,65317],"mapped",[101]],[[65318,65318],"mapped",[102]],[[65319,65319],"mapped",[103]],[[65320,65320],"mapped",[104]],[[65321,65321],"mapped",[105]],[[65322,65322],"mapped",[106]],[[65323,65323],"mapped",[107]],[[65324,65324],"mapped",[108]],[[65325,65325],"mapped",[109]],[[65326,65326],"mapped",[110]],[[65327,65327],"mapped",[111]],[[65328,65328],"mapped",[112]],[[65329,65329],"mapped",[113]],[[65330,65330],"mapped",[114]],[[65331,65331],"mapped",[115]],[[65332,65332],"mapped",[116]],[[65333,65333],"mapped",[117]],[[65334,65334],"mapped",[118]],[[65335,65335],"mapped",[119]],[[65336,65336],"mapped",[120]],[[65337,65337],"mapped",[121]],[[65338,65338],"mapped",[122]],[[65339,65339],"disallowed_STD3_mapped",[91]],[[65340,65340],"disallowed_STD3_mapped",[92]],[[65341,65341],"disallowed_STD3_mapped",[93]],[[65342,65342],"disallowed_STD3_mapped",[94]],[[65343,65343],"disallowed_STD3_mapped",[95]],[[65344,65344],"disallowed_STD3_mapped",[96]],[[65345,65345],"mapped",[97]],[[65346,65346],"mapped",[98]],[[65347,65347],"mapped",[99]],[[65348,65348],"mapped",[100]],[[65349,65349],"mapped",[101]],[[65350,65350],"mapped",[102]],[[65351,65351],"mapped",[103]],[[65352,65352],"mapped",[104]],[[65353,65353],"mapped",[105]],[[65354,65354],"mapped",[106]],[[65355,65355],"mapped",[107]],[[65356,65356],"mapped",[108]],[[65357,65357],"mapped",[109]],[[65358,65358],"mapped",[110]],[[65359,65359],"mapped",[111]],[[65360,65360],"mapped",[112]],[[65361,65361],"mapped",[113]],[[65362,65362],"mapped",[114]],[[65363,65363],"mapped",[115]],[[65364,65364],"mapped",[116]],[[65365,65365],"mapped",[117]],[[65366,65366],"mapped",[118]],[[65367,65367],"mapped",[119]],[[65368,65368],"mapped",[120]],[[65369,65369],"mapped",[121]],[[65370,65370],"mapped",[122]],[[65371,65371],"disallowed_STD3_mapped",[123]],[[65372,65372],"disallowed_STD3_mapped",[124]],[[65373,65373],"disallowed_STD3_mapped",[125]],[[65374,65374],"disallowed_STD3_mapped",[126]],[[65375,65375],"mapped",[10629]],[[65376,65376],"mapped",[10630]],[[65377,65377],"mapped",[46]],[[65378,65378],"mapped",[12300]],[[65379,65379],"mapped",[12301]],[[65380,65380],"mapped",[12289]],[[65381,65381],"mapped",[12539]],[[65382,65382],"mapped",[12530]],[[65383,65383],"mapped",[12449]],[[65384,65384],"mapped",[12451]],[[65385,65385],"mapped",[12453]],[[65386,65386],"mapped",[12455]],[[65387,65387],"mapped",[12457]],[[65388,65388],"mapped",[12515]],[[65389,65389],"mapped",[12517]],[[65390,65390],"mapped",[12519]],[[65391,65391],"mapped",[12483]],[[65392,65392],"mapped",[12540]],[[65393,65393],"mapped",[12450]],[[65394,65394],"mapped",[12452]],[[65395,65395],"mapped",[12454]],[[65396,65396],"mapped",[12456]],[[65397,65397],"mapped",[12458]],[[65398,65398],"mapped",[12459]],[[65399,65399],"mapped",[12461]],[[65400,65400],"mapped",[12463]],[[65401,65401],"mapped",[12465]],[[65402,65402],"mapped",[12467]],[[65403,65403],"mapped",[12469]],[[65404,65404],"mapped",[12471]],[[65405,65405],"mapped",[12473]],[[65406,65406],"mapped",[12475]],[[65407,65407],"mapped",[12477]],[[65408,65408],"mapped",[12479]],[[65409,65409],"mapped",[12481]],[[65410,65410],"mapped",[12484]],[[65411,65411],"mapped",[12486]],[[65412,65412],"mapped",[12488]],[[65413,65413],"mapped",[12490]],[[65414,65414],"mapped",[12491]],[[65415,65415],"mapped",[12492]],[[65416,65416],"mapped",[12493]],[[65417,65417],"mapped",[12494]],[[65418,65418],"mapped",[12495]],[[65419,65419],"mapped",[12498]],[[65420,65420],"mapped",[12501]],[[65421,65421],"mapped",[12504]],[[65422,65422],"mapped",[12507]],[[65423,65423],"mapped",[12510]],[[65424,65424],"mapped",[12511]],[[65425,65425],"mapped",[12512]],[[65426,65426],"mapped",[12513]],[[65427,65427],"mapped",[12514]],[[65428,65428],"mapped",[12516]],[[65429,65429],"mapped",[12518]],[[65430,65430],"mapped",[12520]],[[65431,65431],"mapped",[12521]],[[65432,65432],"mapped",[12522]],[[65433,65433],"mapped",[12523]],[[65434,65434],"mapped",[12524]],[[65435,65435],"mapped",[12525]],[[65436,65436],"mapped",[12527]],[[65437,65437],"mapped",[12531]],[[65438,65438],"mapped",[12441]],[[65439,65439],"mapped",[12442]],[[65440,65440],"disallowed"],[[65441,65441],"mapped",[4352]],[[65442,65442],"mapped",[4353]],[[65443,65443],"mapped",[4522]],[[65444,65444],"mapped",[4354]],[[65445,65445],"mapped",[4524]],[[65446,65446],"mapped",[4525]],[[65447,65447],"mapped",[4355]],[[65448,65448],"mapped",[4356]],[[65449,65449],"mapped",[4357]],[[65450,65450],"mapped",[4528]],[[65451,65451],"mapped",[4529]],[[65452,65452],"mapped",[4530]],[[65453,65453],"mapped",[4531]],[[65454,65454],"mapped",[4532]],[[65455,65455],"mapped",[4533]],[[65456,65456],"mapped",[4378]],[[65457,65457],"mapped",[4358]],[[65458,65458],"mapped",[4359]],[[65459,65459],"mapped",[4360]],[[65460,65460],"mapped",[4385]],[[65461,65461],"mapped",[4361]],[[65462,65462],"mapped",[4362]],[[65463,65463],"mapped",[4363]],[[65464,65464],"mapped",[4364]],[[65465,65465],"mapped",[4365]],[[65466,65466],"mapped",[4366]],[[65467,65467],"mapped",[4367]],[[65468,65468],"mapped",[4368]],[[65469,65469],"mapped",[4369]],[[65470,65470],"mapped",[4370]],[[65471,65473],"disallowed"],[[65474,65474],"mapped",[4449]],[[65475,65475],"mapped",[4450]],[[65476,65476],"mapped",[4451]],[[65477,65477],"mapped",[4452]],[[65478,65478],"mapped",[4453]],[[65479,65479],"mapped",[4454]],[[65480,65481],"disallowed"],[[65482,65482],"mapped",[4455]],[[65483,65483],"mapped",[4456]],[[65484,65484],"mapped",[4457]],[[65485,65485],"mapped",[4458]],[[65486,65486],"mapped",[4459]],[[65487,65487],"mapped",[4460]],[[65488,65489],"disallowed"],[[65490,65490],"mapped",[4461]],[[65491,65491],"mapped",[4462]],[[65492,65492],"mapped",[4463]],[[65493,65493],"mapped",[4464]],[[65494,65494],"mapped",[4465]],[[65495,65495],"mapped",[4466]],[[65496,65497],"disallowed"],[[65498,65498],"mapped",[4467]],[[65499,65499],"mapped",[4468]],[[65500,65500],"mapped",[4469]],[[65501,65503],"disallowed"],[[65504,65504],"mapped",[162]],[[65505,65505],"mapped",[163]],[[65506,65506],"mapped",[172]],[[65507,65507],"disallowed_STD3_mapped",[32,772]],[[65508,65508],"mapped",[166]],[[65509,65509],"mapped",[165]],[[65510,65510],"mapped",[8361]],[[65511,65511],"disallowed"],[[65512,65512],"mapped",[9474]],[[65513,65513],"mapped",[8592]],[[65514,65514],"mapped",[8593]],[[65515,65515],"mapped",[8594]],[[65516,65516],"mapped",[8595]],[[65517,65517],"mapped",[9632]],[[65518,65518],"mapped",[9675]],[[65519,65528],"disallowed"],[[65529,65531],"disallowed"],[[65532,65532],"disallowed"],[[65533,65533],"disallowed"],[[65534,65535],"disallowed"],[[65536,65547],"valid"],[[65548,65548],"disallowed"],[[65549,65574],"valid"],[[65575,65575],"disallowed"],[[65576,65594],"valid"],[[65595,65595],"disallowed"],[[65596,65597],"valid"],[[65598,65598],"disallowed"],[[65599,65613],"valid"],[[65614,65615],"disallowed"],[[65616,65629],"valid"],[[65630,65663],"disallowed"],[[65664,65786],"valid"],[[65787,65791],"disallowed"],[[65792,65794],"valid",[],"NV8"],[[65795,65798],"disallowed"],[[65799,65843],"valid",[],"NV8"],[[65844,65846],"disallowed"],[[65847,65855],"valid",[],"NV8"],[[65856,65930],"valid",[],"NV8"],[[65931,65932],"valid",[],"NV8"],[[65933,65935],"disallowed"],[[65936,65947],"valid",[],"NV8"],[[65948,65951],"disallowed"],[[65952,65952],"valid",[],"NV8"],[[65953,65999],"disallowed"],[[66000,66044],"valid",[],"NV8"],[[66045,66045],"valid"],[[66046,66175],"disallowed"],[[66176,66204],"valid"],[[66205,66207],"disallowed"],[[66208,66256],"valid"],[[66257,66271],"disallowed"],[[66272,66272],"valid"],[[66273,66299],"valid",[],"NV8"],[[66300,66303],"disallowed"],[[66304,66334],"valid"],[[66335,66335],"valid"],[[66336,66339],"valid",[],"NV8"],[[66340,66351],"disallowed"],[[66352,66368],"valid"],[[66369,66369],"valid",[],"NV8"],[[66370,66377],"valid"],[[66378,66378],"valid",[],"NV8"],[[66379,66383],"disallowed"],[[66384,66426],"valid"],[[66427,66431],"disallowed"],[[66432,66461],"valid"],[[66462,66462],"disallowed"],[[66463,66463],"valid",[],"NV8"],[[66464,66499],"valid"],[[66500,66503],"disallowed"],[[66504,66511],"valid"],[[66512,66517],"valid",[],"NV8"],[[66518,66559],"disallowed"],[[66560,66560],"mapped",[66600]],[[66561,66561],"mapped",[66601]],[[66562,66562],"mapped",[66602]],[[66563,66563],"mapped",[66603]],[[66564,66564],"mapped",[66604]],[[66565,66565],"mapped",[66605]],[[66566,66566],"mapped",[66606]],[[66567,66567],"mapped",[66607]],[[66568,66568],"mapped",[66608]],[[66569,66569],"mapped",[66609]],[[66570,66570],"mapped",[66610]],[[66571,66571],"mapped",[66611]],[[66572,66572],"mapped",[66612]],[[66573,66573],"mapped",[66613]],[[66574,66574],"mapped",[66614]],[[66575,66575],"mapped",[66615]],[[66576,66576],"mapped",[66616]],[[66577,66577],"mapped",[66617]],[[66578,66578],"mapped",[66618]],[[66579,66579],"mapped",[66619]],[[66580,66580],"mapped",[66620]],[[66581,66581],"mapped",[66621]],[[66582,66582],"mapped",[66622]],[[66583,66583],"mapped",[66623]],[[66584,66584],"mapped",[66624]],[[66585,66585],"mapped",[66625]],[[66586,66586],"mapped",[66626]],[[66587,66587],"mapped",[66627]],[[66588,66588],"mapped",[66628]],[[66589,66589],"mapped",[66629]],[[66590,66590],"mapped",[66630]],[[66591,66591],"mapped",[66631]],[[66592,66592],"mapped",[66632]],[[66593,66593],"mapped",[66633]],[[66594,66594],"mapped",[66634]],[[66595,66595],"mapped",[66635]],[[66596,66596],"mapped",[66636]],[[66597,66597],"mapped",[66637]],[[66598,66598],"mapped",[66638]],[[66599,66599],"mapped",[66639]],[[66600,66637],"valid"],[[66638,66717],"valid"],[[66718,66719],"disallowed"],[[66720,66729],"valid"],[[66730,66815],"disallowed"],[[66816,66855],"valid"],[[66856,66863],"disallowed"],[[66864,66915],"valid"],[[66916,66926],"disallowed"],[[66927,66927],"valid",[],"NV8"],[[66928,67071],"disallowed"],[[67072,67382],"valid"],[[67383,67391],"disallowed"],[[67392,67413],"valid"],[[67414,67423],"disallowed"],[[67424,67431],"valid"],[[67432,67583],"disallowed"],[[67584,67589],"valid"],[[67590,67591],"disallowed"],[[67592,67592],"valid"],[[67593,67593],"disallowed"],[[67594,67637],"valid"],[[67638,67638],"disallowed"],[[67639,67640],"valid"],[[67641,67643],"disallowed"],[[67644,67644],"valid"],[[67645,67646],"disallowed"],[[67647,67647],"valid"],[[67648,67669],"valid"],[[67670,67670],"disallowed"],[[67671,67679],"valid",[],"NV8"],[[67680,67702],"valid"],[[67703,67711],"valid",[],"NV8"],[[67712,67742],"valid"],[[67743,67750],"disallowed"],[[67751,67759],"valid",[],"NV8"],[[67760,67807],"disallowed"],[[67808,67826],"valid"],[[67827,67827],"disallowed"],[[67828,67829],"valid"],[[67830,67834],"disallowed"],[[67835,67839],"valid",[],"NV8"],[[67840,67861],"valid"],[[67862,67865],"valid",[],"NV8"],[[67866,67867],"valid",[],"NV8"],[[67868,67870],"disallowed"],[[67871,67871],"valid",[],"NV8"],[[67872,67897],"valid"],[[67898,67902],"disallowed"],[[67903,67903],"valid",[],"NV8"],[[67904,67967],"disallowed"],[[67968,68023],"valid"],[[68024,68027],"disallowed"],[[68028,68029],"valid",[],"NV8"],[[68030,68031],"valid"],[[68032,68047],"valid",[],"NV8"],[[68048,68049],"disallowed"],[[68050,68095],"valid",[],"NV8"],[[68096,68099],"valid"],[[68100,68100],"disallowed"],[[68101,68102],"valid"],[[68103,68107],"disallowed"],[[68108,68115],"valid"],[[68116,68116],"disallowed"],[[68117,68119],"valid"],[[68120,68120],"disallowed"],[[68121,68147],"valid"],[[68148,68151],"disallowed"],[[68152,68154],"valid"],[[68155,68158],"disallowed"],[[68159,68159],"valid"],[[68160,68167],"valid",[],"NV8"],[[68168,68175],"disallowed"],[[68176,68184],"valid",[],"NV8"],[[68185,68191],"disallowed"],[[68192,68220],"valid"],[[68221,68223],"valid",[],"NV8"],[[68224,68252],"valid"],[[68253,68255],"valid",[],"NV8"],[[68256,68287],"disallowed"],[[68288,68295],"valid"],[[68296,68296],"valid",[],"NV8"],[[68297,68326],"valid"],[[68327,68330],"disallowed"],[[68331,68342],"valid",[],"NV8"],[[68343,68351],"disallowed"],[[68352,68405],"valid"],[[68406,68408],"disallowed"],[[68409,68415],"valid",[],"NV8"],[[68416,68437],"valid"],[[68438,68439],"disallowed"],[[68440,68447],"valid",[],"NV8"],[[68448,68466],"valid"],[[68467,68471],"disallowed"],[[68472,68479],"valid",[],"NV8"],[[68480,68497],"valid"],[[68498,68504],"disallowed"],[[68505,68508],"valid",[],"NV8"],[[68509,68520],"disallowed"],[[68521,68527],"valid",[],"NV8"],[[68528,68607],"disallowed"],[[68608,68680],"valid"],[[68681,68735],"disallowed"],[[68736,68736],"mapped",[68800]],[[68737,68737],"mapped",[68801]],[[68738,68738],"mapped",[68802]],[[68739,68739],"mapped",[68803]],[[68740,68740],"mapped",[68804]],[[68741,68741],"mapped",[68805]],[[68742,68742],"mapped",[68806]],[[68743,68743],"mapped",[68807]],[[68744,68744],"mapped",[68808]],[[68745,68745],"mapped",[68809]],[[68746,68746],"mapped",[68810]],[[68747,68747],"mapped",[68811]],[[68748,68748],"mapped",[68812]],[[68749,68749],"mapped",[68813]],[[68750,68750],"mapped",[68814]],[[68751,68751],"mapped",[68815]],[[68752,68752],"mapped",[68816]],[[68753,68753],"mapped",[68817]],[[68754,68754],"mapped",[68818]],[[68755,68755],"mapped",[68819]],[[68756,68756],"mapped",[68820]],[[68757,68757],"mapped",[68821]],[[68758,68758],"mapped",[68822]],[[68759,68759],"mapped",[68823]],[[68760,68760],"mapped",[68824]],[[68761,68761],"mapped",[68825]],[[68762,68762],"mapped",[68826]],[[68763,68763],"mapped",[68827]],[[68764,68764],"mapped",[68828]],[[68765,68765],"mapped",[68829]],[[68766,68766],"mapped",[68830]],[[68767,68767],"mapped",[68831]],[[68768,68768],"mapped",[68832]],[[68769,68769],"mapped",[68833]],[[68770,68770],"mapped",[68834]],[[68771,68771],"mapped",[68835]],[[68772,68772],"mapped",[68836]],[[68773,68773],"mapped",[68837]],[[68774,68774],"mapped",[68838]],[[68775,68775],"mapped",[68839]],[[68776,68776],"mapped",[68840]],[[68777,68777],"mapped",[68841]],[[68778,68778],"mapped",[68842]],[[68779,68779],"mapped",[68843]],[[68780,68780],"mapped",[68844]],[[68781,68781],"mapped",[68845]],[[68782,68782],"mapped",[68846]],[[68783,68783],"mapped",[68847]],[[68784,68784],"mapped",[68848]],[[68785,68785],"mapped",[68849]],[[68786,68786],"mapped",[68850]],[[68787,68799],"disallowed"],[[68800,68850],"valid"],[[68851,68857],"disallowed"],[[68858,68863],"valid",[],"NV8"],[[68864,69215],"disallowed"],[[69216,69246],"valid",[],"NV8"],[[69247,69631],"disallowed"],[[69632,69702],"valid"],[[69703,69709],"valid",[],"NV8"],[[69710,69713],"disallowed"],[[69714,69733],"valid",[],"NV8"],[[69734,69743],"valid"],[[69744,69758],"disallowed"],[[69759,69759],"valid"],[[69760,69818],"valid"],[[69819,69820],"valid",[],"NV8"],[[69821,69821],"disallowed"],[[69822,69825],"valid",[],"NV8"],[[69826,69839],"disallowed"],[[69840,69864],"valid"],[[69865,69871],"disallowed"],[[69872,69881],"valid"],[[69882,69887],"disallowed"],[[69888,69940],"valid"],[[69941,69941],"disallowed"],[[69942,69951],"valid"],[[69952,69955],"valid",[],"NV8"],[[69956,69967],"disallowed"],[[69968,70003],"valid"],[[70004,70005],"valid",[],"NV8"],[[70006,70006],"valid"],[[70007,70015],"disallowed"],[[70016,70084],"valid"],[[70085,70088],"valid",[],"NV8"],[[70089,70089],"valid",[],"NV8"],[[70090,70092],"valid"],[[70093,70093],"valid",[],"NV8"],[[70094,70095],"disallowed"],[[70096,70105],"valid"],[[70106,70106],"valid"],[[70107,70107],"valid",[],"NV8"],[[70108,70108],"valid"],[[70109,70111],"valid",[],"NV8"],[[70112,70112],"disallowed"],[[70113,70132],"valid",[],"NV8"],[[70133,70143],"disallowed"],[[70144,70161],"valid"],[[70162,70162],"disallowed"],[[70163,70199],"valid"],[[70200,70205],"valid",[],"NV8"],[[70206,70271],"disallowed"],[[70272,70278],"valid"],[[70279,70279],"disallowed"],[[70280,70280],"valid"],[[70281,70281],"disallowed"],[[70282,70285],"valid"],[[70286,70286],"disallowed"],[[70287,70301],"valid"],[[70302,70302],"disallowed"],[[70303,70312],"valid"],[[70313,70313],"valid",[],"NV8"],[[70314,70319],"disallowed"],[[70320,70378],"valid"],[[70379,70383],"disallowed"],[[70384,70393],"valid"],[[70394,70399],"disallowed"],[[70400,70400],"valid"],[[70401,70403],"valid"],[[70404,70404],"disallowed"],[[70405,70412],"valid"],[[70413,70414],"disallowed"],[[70415,70416],"valid"],[[70417,70418],"disallowed"],[[70419,70440],"valid"],[[70441,70441],"disallowed"],[[70442,70448],"valid"],[[70449,70449],"disallowed"],[[70450,70451],"valid"],[[70452,70452],"disallowed"],[[70453,70457],"valid"],[[70458,70459],"disallowed"],[[70460,70468],"valid"],[[70469,70470],"disallowed"],[[70471,70472],"valid"],[[70473,70474],"disallowed"],[[70475,70477],"valid"],[[70478,70479],"disallowed"],[[70480,70480],"valid"],[[70481,70486],"disallowed"],[[70487,70487],"valid"],[[70488,70492],"disallowed"],[[70493,70499],"valid"],[[70500,70501],"disallowed"],[[70502,70508],"valid"],[[70509,70511],"disallowed"],[[70512,70516],"valid"],[[70517,70783],"disallowed"],[[70784,70853],"valid"],[[70854,70854],"valid",[],"NV8"],[[70855,70855],"valid"],[[70856,70863],"disallowed"],[[70864,70873],"valid"],[[70874,71039],"disallowed"],[[71040,71093],"valid"],[[71094,71095],"disallowed"],[[71096,71104],"valid"],[[71105,71113],"valid",[],"NV8"],[[71114,71127],"valid",[],"NV8"],[[71128,71133],"valid"],[[71134,71167],"disallowed"],[[71168,71232],"valid"],[[71233,71235],"valid",[],"NV8"],[[71236,71236],"valid"],[[71237,71247],"disallowed"],[[71248,71257],"valid"],[[71258,71295],"disallowed"],[[71296,71351],"valid"],[[71352,71359],"disallowed"],[[71360,71369],"valid"],[[71370,71423],"disallowed"],[[71424,71449],"valid"],[[71450,71452],"disallowed"],[[71453,71467],"valid"],[[71468,71471],"disallowed"],[[71472,71481],"valid"],[[71482,71487],"valid",[],"NV8"],[[71488,71839],"disallowed"],[[71840,71840],"mapped",[71872]],[[71841,71841],"mapped",[71873]],[[71842,71842],"mapped",[71874]],[[71843,71843],"mapped",[71875]],[[71844,71844],"mapped",[71876]],[[71845,71845],"mapped",[71877]],[[71846,71846],"mapped",[71878]],[[71847,71847],"mapped",[71879]],[[71848,71848],"mapped",[71880]],[[71849,71849],"mapped",[71881]],[[71850,71850],"mapped",[71882]],[[71851,71851],"mapped",[71883]],[[71852,71852],"mapped",[71884]],[[71853,71853],"mapped",[71885]],[[71854,71854],"mapped",[71886]],[[71855,71855],"mapped",[71887]],[[71856,71856],"mapped",[71888]],[[71857,71857],"mapped",[71889]],[[71858,71858],"mapped",[71890]],[[71859,71859],"mapped",[71891]],[[71860,71860],"mapped",[71892]],[[71861,71861],"mapped",[71893]],[[71862,71862],"mapped",[71894]],[[71863,71863],"mapped",[71895]],[[71864,71864],"mapped",[71896]],[[71865,71865],"mapped",[71897]],[[71866,71866],"mapped",[71898]],[[71867,71867],"mapped",[71899]],[[71868,71868],"mapped",[71900]],[[71869,71869],"mapped",[71901]],[[71870,71870],"mapped",[71902]],[[71871,71871],"mapped",[71903]],[[71872,71913],"valid"],[[71914,71922],"valid",[],"NV8"],[[71923,71934],"disallowed"],[[71935,71935],"valid"],[[71936,72383],"disallowed"],[[72384,72440],"valid"],[[72441,73727],"disallowed"],[[73728,74606],"valid"],[[74607,74648],"valid"],[[74649,74649],"valid"],[[74650,74751],"disallowed"],[[74752,74850],"valid",[],"NV8"],[[74851,74862],"valid",[],"NV8"],[[74863,74863],"disallowed"],[[74864,74867],"valid",[],"NV8"],[[74868,74868],"valid",[],"NV8"],[[74869,74879],"disallowed"],[[74880,75075],"valid"],[[75076,77823],"disallowed"],[[77824,78894],"valid"],[[78895,82943],"disallowed"],[[82944,83526],"valid"],[[83527,92159],"disallowed"],[[92160,92728],"valid"],[[92729,92735],"disallowed"],[[92736,92766],"valid"],[[92767,92767],"disallowed"],[[92768,92777],"valid"],[[92778,92781],"disallowed"],[[92782,92783],"valid",[],"NV8"],[[92784,92879],"disallowed"],[[92880,92909],"valid"],[[92910,92911],"disallowed"],[[92912,92916],"valid"],[[92917,92917],"valid",[],"NV8"],[[92918,92927],"disallowed"],[[92928,92982],"valid"],[[92983,92991],"valid",[],"NV8"],[[92992,92995],"valid"],[[92996,92997],"valid",[],"NV8"],[[92998,93007],"disallowed"],[[93008,93017],"valid"],[[93018,93018],"disallowed"],[[93019,93025],"valid",[],"NV8"],[[93026,93026],"disallowed"],[[93027,93047],"valid"],[[93048,93052],"disallowed"],[[93053,93071],"valid"],[[93072,93951],"disallowed"],[[93952,94020],"valid"],[[94021,94031],"disallowed"],[[94032,94078],"valid"],[[94079,94094],"disallowed"],[[94095,94111],"valid"],[[94112,110591],"disallowed"],[[110592,110593],"valid"],[[110594,113663],"disallowed"],[[113664,113770],"valid"],[[113771,113775],"disallowed"],[[113776,113788],"valid"],[[113789,113791],"disallowed"],[[113792,113800],"valid"],[[113801,113807],"disallowed"],[[113808,113817],"valid"],[[113818,113819],"disallowed"],[[113820,113820],"valid",[],"NV8"],[[113821,113822],"valid"],[[113823,113823],"valid",[],"NV8"],[[113824,113827],"ignored"],[[113828,118783],"disallowed"],[[118784,119029],"valid",[],"NV8"],[[119030,119039],"disallowed"],[[119040,119078],"valid",[],"NV8"],[[119079,119080],"disallowed"],[[119081,119081],"valid",[],"NV8"],[[119082,119133],"valid",[],"NV8"],[[119134,119134],"mapped",[119127,119141]],[[119135,119135],"mapped",[119128,119141]],[[119136,119136],"mapped",[119128,119141,119150]],[[119137,119137],"mapped",[119128,119141,119151]],[[119138,119138],"mapped",[119128,119141,119152]],[[119139,119139],"mapped",[119128,119141,119153]],[[119140,119140],"mapped",[119128,119141,119154]],[[119141,119154],"valid",[],"NV8"],[[119155,119162],"disallowed"],[[119163,119226],"valid",[],"NV8"],[[119227,119227],"mapped",[119225,119141]],[[119228,119228],"mapped",[119226,119141]],[[119229,119229],"mapped",[119225,119141,119150]],[[119230,119230],"mapped",[119226,119141,119150]],[[119231,119231],"mapped",[119225,119141,119151]],[[119232,119232],"mapped",[119226,119141,119151]],[[119233,119261],"valid",[],"NV8"],[[119262,119272],"valid",[],"NV8"],[[119273,119295],"disallowed"],[[119296,119365],"valid",[],"NV8"],[[119366,119551],"disallowed"],[[119552,119638],"valid",[],"NV8"],[[119639,119647],"disallowed"],[[119648,119665],"valid",[],"NV8"],[[119666,119807],"disallowed"],[[119808,119808],"mapped",[97]],[[119809,119809],"mapped",[98]],[[119810,119810],"mapped",[99]],[[119811,119811],"mapped",[100]],[[119812,119812],"mapped",[101]],[[119813,119813],"mapped",[102]],[[119814,119814],"mapped",[103]],[[119815,119815],"mapped",[104]],[[119816,119816],"mapped",[105]],[[119817,119817],"mapped",[106]],[[119818,119818],"mapped",[107]],[[119819,119819],"mapped",[108]],[[119820,119820],"mapped",[109]],[[119821,119821],"mapped",[110]],[[119822,119822],"mapped",[111]],[[119823,119823],"mapped",[112]],[[119824,119824],"mapped",[113]],[[119825,119825],"mapped",[114]],[[119826,119826],"mapped",[115]],[[119827,119827],"mapped",[116]],[[119828,119828],"mapped",[117]],[[119829,119829],"mapped",[118]],[[119830,119830],"mapped",[119]],[[119831,119831],"mapped",[120]],[[119832,119832],"mapped",[121]],[[119833,119833],"mapped",[122]],[[119834,119834],"mapped",[97]],[[119835,119835],"mapped",[98]],[[119836,119836],"mapped",[99]],[[119837,119837],"mapped",[100]],[[119838,119838],"mapped",[101]],[[119839,119839],"mapped",[102]],[[119840,119840],"mapped",[103]],[[119841,119841],"mapped",[104]],[[119842,119842],"mapped",[105]],[[119843,119843],"mapped",[106]],[[119844,119844],"mapped",[107]],[[119845,119845],"mapped",[108]],[[119846,119846],"mapped",[109]],[[119847,119847],"mapped",[110]],[[119848,119848],"mapped",[111]],[[119849,119849],"mapped",[112]],[[119850,119850],"mapped",[113]],[[119851,119851],"mapped",[114]],[[119852,119852],"mapped",[115]],[[119853,119853],"mapped",[116]],[[119854,119854],"mapped",[117]],[[119855,119855],"mapped",[118]],[[119856,119856],"mapped",[119]],[[119857,119857],"mapped",[120]],[[119858,119858],"mapped",[121]],[[119859,119859],"mapped",[122]],[[119860,119860],"mapped",[97]],[[119861,119861],"mapped",[98]],[[119862,119862],"mapped",[99]],[[119863,119863],"mapped",[100]],[[119864,119864],"mapped",[101]],[[119865,119865],"mapped",[102]],[[119866,119866],"mapped",[103]],[[119867,119867],"mapped",[104]],[[119868,119868],"mapped",[105]],[[119869,119869],"mapped",[106]],[[119870,119870],"mapped",[107]],[[119871,119871],"mapped",[108]],[[119872,119872],"mapped",[109]],[[119873,119873],"mapped",[110]],[[119874,119874],"mapped",[111]],[[119875,119875],"mapped",[112]],[[119876,119876],"mapped",[113]],[[119877,119877],"mapped",[114]],[[119878,119878],"mapped",[115]],[[119879,119879],"mapped",[116]],[[119880,119880],"mapped",[117]],[[119881,119881],"mapped",[118]],[[119882,119882],"mapped",[119]],[[119883,119883],"mapped",[120]],[[119884,119884],"mapped",[121]],[[119885,119885],"mapped",[122]],[[119886,119886],"mapped",[97]],[[119887,119887],"mapped",[98]],[[119888,119888],"mapped",[99]],[[119889,119889],"mapped",[100]],[[119890,119890],"mapped",[101]],[[119891,119891],"mapped",[102]],[[119892,119892],"mapped",[103]],[[119893,119893],"disallowed"],[[119894,119894],"mapped",[105]],[[119895,119895],"mapped",[106]],[[119896,119896],"mapped",[107]],[[119897,119897],"mapped",[108]],[[119898,119898],"mapped",[109]],[[119899,119899],"mapped",[110]],[[119900,119900],"mapped",[111]],[[119901,119901],"mapped",[112]],[[119902,119902],"mapped",[113]],[[119903,119903],"mapped",[114]],[[119904,119904],"mapped",[115]],[[119905,119905],"mapped",[116]],[[119906,119906],"mapped",[117]],[[119907,119907],"mapped",[118]],[[119908,119908],"mapped",[119]],[[119909,119909],"mapped",[120]],[[119910,119910],"mapped",[121]],[[119911,119911],"mapped",[122]],[[119912,119912],"mapped",[97]],[[119913,119913],"mapped",[98]],[[119914,119914],"mapped",[99]],[[119915,119915],"mapped",[100]],[[119916,119916],"mapped",[101]],[[119917,119917],"mapped",[102]],[[119918,119918],"mapped",[103]],[[119919,119919],"mapped",[104]],[[119920,119920],"mapped",[105]],[[119921,119921],"mapped",[106]],[[119922,119922],"mapped",[107]],[[119923,119923],"mapped",[108]],[[119924,119924],"mapped",[109]],[[119925,119925],"mapped",[110]],[[119926,119926],"mapped",[111]],[[119927,119927],"mapped",[112]],[[119928,119928],"mapped",[113]],[[119929,119929],"mapped",[114]],[[119930,119930],"mapped",[115]],[[119931,119931],"mapped",[116]],[[119932,119932],"mapped",[117]],[[119933,119933],"mapped",[118]],[[119934,119934],"mapped",[119]],[[119935,119935],"mapped",[120]],[[119936,119936],"mapped",[121]],[[119937,119937],"mapped",[122]],[[119938,119938],"mapped",[97]],[[119939,119939],"mapped",[98]],[[119940,119940],"mapped",[99]],[[119941,119941],"mapped",[100]],[[119942,119942],"mapped",[101]],[[119943,119943],"mapped",[102]],[[119944,119944],"mapped",[103]],[[119945,119945],"mapped",[104]],[[119946,119946],"mapped",[105]],[[119947,119947],"mapped",[106]],[[119948,119948],"mapped",[107]],[[119949,119949],"mapped",[108]],[[119950,119950],"mapped",[109]],[[119951,119951],"mapped",[110]],[[119952,119952],"mapped",[111]],[[119953,119953],"mapped",[112]],[[119954,119954],"mapped",[113]],[[119955,119955],"mapped",[114]],[[119956,119956],"mapped",[115]],[[119957,119957],"mapped",[116]],[[119958,119958],"mapped",[117]],[[119959,119959],"mapped",[118]],[[119960,119960],"mapped",[119]],[[119961,119961],"mapped",[120]],[[119962,119962],"mapped",[121]],[[119963,119963],"mapped",[122]],[[119964,119964],"mapped",[97]],[[119965,119965],"disallowed"],[[119966,119966],"mapped",[99]],[[119967,119967],"mapped",[100]],[[119968,119969],"disallowed"],[[119970,119970],"mapped",[103]],[[119971,119972],"disallowed"],[[119973,119973],"mapped",[106]],[[119974,119974],"mapped",[107]],[[119975,119976],"disallowed"],[[119977,119977],"mapped",[110]],[[119978,119978],"mapped",[111]],[[119979,119979],"mapped",[112]],[[119980,119980],"mapped",[113]],[[119981,119981],"disallowed"],[[119982,119982],"mapped",[115]],[[119983,119983],"mapped",[116]],[[119984,119984],"mapped",[117]],[[119985,119985],"mapped",[118]],[[119986,119986],"mapped",[119]],[[119987,119987],"mapped",[120]],[[119988,119988],"mapped",[121]],[[119989,119989],"mapped",[122]],[[119990,119990],"mapped",[97]],[[119991,119991],"mapped",[98]],[[119992,119992],"mapped",[99]],[[119993,119993],"mapped",[100]],[[119994,119994],"disallowed"],[[119995,119995],"mapped",[102]],[[119996,119996],"disallowed"],[[119997,119997],"mapped",[104]],[[119998,119998],"mapped",[105]],[[119999,119999],"mapped",[106]],[[120000,120000],"mapped",[107]],[[120001,120001],"mapped",[108]],[[120002,120002],"mapped",[109]],[[120003,120003],"mapped",[110]],[[120004,120004],"disallowed"],[[120005,120005],"mapped",[112]],[[120006,120006],"mapped",[113]],[[120007,120007],"mapped",[114]],[[120008,120008],"mapped",[115]],[[120009,120009],"mapped",[116]],[[120010,120010],"mapped",[117]],[[120011,120011],"mapped",[118]],[[120012,120012],"mapped",[119]],[[120013,120013],"mapped",[120]],[[120014,120014],"mapped",[121]],[[120015,120015],"mapped",[122]],[[120016,120016],"mapped",[97]],[[120017,120017],"mapped",[98]],[[120018,120018],"mapped",[99]],[[120019,120019],"mapped",[100]],[[120020,120020],"mapped",[101]],[[120021,120021],"mapped",[102]],[[120022,120022],"mapped",[103]],[[120023,120023],"mapped",[104]],[[120024,120024],"mapped",[105]],[[120025,120025],"mapped",[106]],[[120026,120026],"mapped",[107]],[[120027,120027],"mapped",[108]],[[120028,120028],"mapped",[109]],[[120029,120029],"mapped",[110]],[[120030,120030],"mapped",[111]],[[120031,120031],"mapped",[112]],[[120032,120032],"mapped",[113]],[[120033,120033],"mapped",[114]],[[120034,120034],"mapped",[115]],[[120035,120035],"mapped",[116]],[[120036,120036],"mapped",[117]],[[120037,120037],"mapped",[118]],[[120038,120038],"mapped",[119]],[[120039,120039],"mapped",[120]],[[120040,120040],"mapped",[121]],[[120041,120041],"mapped",[122]],[[120042,120042],"mapped",[97]],[[120043,120043],"mapped",[98]],[[120044,120044],"mapped",[99]],[[120045,120045],"mapped",[100]],[[120046,120046],"mapped",[101]],[[120047,120047],"mapped",[102]],[[120048,120048],"mapped",[103]],[[120049,120049],"mapped",[104]],[[120050,120050],"mapped",[105]],[[120051,120051],"mapped",[106]],[[120052,120052],"mapped",[107]],[[120053,120053],"mapped",[108]],[[120054,120054],"mapped",[109]],[[120055,120055],"mapped",[110]],[[120056,120056],"mapped",[111]],[[120057,120057],"mapped",[112]],[[120058,120058],"mapped",[113]],[[120059,120059],"mapped",[114]],[[120060,120060],"mapped",[115]],[[120061,120061],"mapped",[116]],[[120062,120062],"mapped",[117]],[[120063,120063],"mapped",[118]],[[120064,120064],"mapped",[119]],[[120065,120065],"mapped",[120]],[[120066,120066],"mapped",[121]],[[120067,120067],"mapped",[122]],[[120068,120068],"mapped",[97]],[[120069,120069],"mapped",[98]],[[120070,120070],"disallowed"],[[120071,120071],"mapped",[100]],[[120072,120072],"mapped",[101]],[[120073,120073],"mapped",[102]],[[120074,120074],"mapped",[103]],[[120075,120076],"disallowed"],[[120077,120077],"mapped",[106]],[[120078,120078],"mapped",[107]],[[120079,120079],"mapped",[108]],[[120080,120080],"mapped",[109]],[[120081,120081],"mapped",[110]],[[120082,120082],"mapped",[111]],[[120083,120083],"mapped",[112]],[[120084,120084],"mapped",[113]],[[120085,120085],"disallowed"],[[120086,120086],"mapped",[115]],[[120087,120087],"mapped",[116]],[[120088,120088],"mapped",[117]],[[120089,120089],"mapped",[118]],[[120090,120090],"mapped",[119]],[[120091,120091],"mapped",[120]],[[120092,120092],"mapped",[121]],[[120093,120093],"disallowed"],[[120094,120094],"mapped",[97]],[[120095,120095],"mapped",[98]],[[120096,120096],"mapped",[99]],[[120097,120097],"mapped",[100]],[[120098,120098],"mapped",[101]],[[120099,120099],"mapped",[102]],[[120100,120100],"mapped",[103]],[[120101,120101],"mapped",[104]],[[120102,120102],"mapped",[105]],[[120103,120103],"mapped",[106]],[[120104,120104],"mapped",[107]],[[120105,120105],"mapped",[108]],[[120106,120106],"mapped",[109]],[[120107,120107],"mapped",[110]],[[120108,120108],"mapped",[111]],[[120109,120109],"mapped",[112]],[[120110,120110],"mapped",[113]],[[120111,120111],"mapped",[114]],[[120112,120112],"mapped",[115]],[[120113,120113],"mapped",[116]],[[120114,120114],"mapped",[117]],[[120115,120115],"mapped",[118]],[[120116,120116],"mapped",[119]],[[120117,120117],"mapped",[120]],[[120118,120118],"mapped",[121]],[[120119,120119],"mapped",[122]],[[120120,120120],"mapped",[97]],[[120121,120121],"mapped",[98]],[[120122,120122],"disallowed"],[[120123,120123],"mapped",[100]],[[120124,120124],"mapped",[101]],[[120125,120125],"mapped",[102]],[[120126,120126],"mapped",[103]],[[120127,120127],"disallowed"],[[120128,120128],"mapped",[105]],[[120129,120129],"mapped",[106]],[[120130,120130],"mapped",[107]],[[120131,120131],"mapped",[108]],[[120132,120132],"mapped",[109]],[[120133,120133],"disallowed"],[[120134,120134],"mapped",[111]],[[120135,120137],"disallowed"],[[120138,120138],"mapped",[115]],[[120139,120139],"mapped",[116]],[[120140,120140],"mapped",[117]],[[120141,120141],"mapped",[118]],[[120142,120142],"mapped",[119]],[[120143,120143],"mapped",[120]],[[120144,120144],"mapped",[121]],[[120145,120145],"disallowed"],[[120146,120146],"mapped",[97]],[[120147,120147],"mapped",[98]],[[120148,120148],"mapped",[99]],[[120149,120149],"mapped",[100]],[[120150,120150],"mapped",[101]],[[120151,120151],"mapped",[102]],[[120152,120152],"mapped",[103]],[[120153,120153],"mapped",[104]],[[120154,120154],"mapped",[105]],[[120155,120155],"mapped",[106]],[[120156,120156],"mapped",[107]],[[120157,120157],"mapped",[108]],[[120158,120158],"mapped",[109]],[[120159,120159],"mapped",[110]],[[120160,120160],"mapped",[111]],[[120161,120161],"mapped",[112]],[[120162,120162],"mapped",[113]],[[120163,120163],"mapped",[114]],[[120164,120164],"mapped",[115]],[[120165,120165],"mapped",[116]],[[120166,120166],"mapped",[117]],[[120167,120167],"mapped",[118]],[[120168,120168],"mapped",[119]],[[120169,120169],"mapped",[120]],[[120170,120170],"mapped",[121]],[[120171,120171],"mapped",[122]],[[120172,120172],"mapped",[97]],[[120173,120173],"mapped",[98]],[[120174,120174],"mapped",[99]],[[120175,120175],"mapped",[100]],[[120176,120176],"mapped",[101]],[[120177,120177],"mapped",[102]],[[120178,120178],"mapped",[103]],[[120179,120179],"mapped",[104]],[[120180,120180],"mapped",[105]],[[120181,120181],"mapped",[106]],[[120182,120182],"mapped",[107]],[[120183,120183],"mapped",[108]],[[120184,120184],"mapped",[109]],[[120185,120185],"mapped",[110]],[[120186,120186],"mapped",[111]],[[120187,120187],"mapped",[112]],[[120188,120188],"mapped",[113]],[[120189,120189],"mapped",[114]],[[120190,120190],"mapped",[115]],[[120191,120191],"mapped",[116]],[[120192,120192],"mapped",[117]],[[120193,120193],"mapped",[118]],[[120194,120194],"mapped",[119]],[[120195,120195],"mapped",[120]],[[120196,120196],"mapped",[121]],[[120197,120197],"mapped",[122]],[[120198,120198],"mapped",[97]],[[120199,120199],"mapped",[98]],[[120200,120200],"mapped",[99]],[[120201,120201],"mapped",[100]],[[120202,120202],"mapped",[101]],[[120203,120203],"mapped",[102]],[[120204,120204],"mapped",[103]],[[120205,120205],"mapped",[104]],[[120206,120206],"mapped",[105]],[[120207,120207],"mapped",[106]],[[120208,120208],"mapped",[107]],[[120209,120209],"mapped",[108]],[[120210,120210],"mapped",[109]],[[120211,120211],"mapped",[110]],[[120212,120212],"mapped",[111]],[[120213,120213],"mapped",[112]],[[120214,120214],"mapped",[113]],[[120215,120215],"mapped",[114]],[[120216,120216],"mapped",[115]],[[120217,120217],"mapped",[116]],[[120218,120218],"mapped",[117]],[[120219,120219],"mapped",[118]],[[120220,120220],"mapped",[119]],[[120221,120221],"mapped",[120]],[[120222,120222],"mapped",[121]],[[120223,120223],"mapped",[122]],[[120224,120224],"mapped",[97]],[[120225,120225],"mapped",[98]],[[120226,120226],"mapped",[99]],[[120227,120227],"mapped",[100]],[[120228,120228],"mapped",[101]],[[120229,120229],"mapped",[102]],[[120230,120230],"mapped",[103]],[[120231,120231],"mapped",[104]],[[120232,120232],"mapped",[105]],[[120233,120233],"mapped",[106]],[[120234,120234],"mapped",[107]],[[120235,120235],"mapped",[108]],[[120236,120236],"mapped",[109]],[[120237,120237],"mapped",[110]],[[120238,120238],"mapped",[111]],[[120239,120239],"mapped",[112]],[[120240,120240],"mapped",[113]],[[120241,120241],"mapped",[114]],[[120242,120242],"mapped",[115]],[[120243,120243],"mapped",[116]],[[120244,120244],"mapped",[117]],[[120245,120245],"mapped",[118]],[[120246,120246],"mapped",[119]],[[120247,120247],"mapped",[120]],[[120248,120248],"mapped",[121]],[[120249,120249],"mapped",[122]],[[120250,120250],"mapped",[97]],[[120251,120251],"mapped",[98]],[[120252,120252],"mapped",[99]],[[120253,120253],"mapped",[100]],[[120254,120254],"mapped",[101]],[[120255,120255],"mapped",[102]],[[120256,120256],"mapped",[103]],[[120257,120257],"mapped",[104]],[[120258,120258],"mapped",[105]],[[120259,120259],"mapped",[106]],[[120260,120260],"mapped",[107]],[[120261,120261],"mapped",[108]],[[120262,120262],"mapped",[109]],[[120263,120263],"mapped",[110]],[[120264,120264],"mapped",[111]],[[120265,120265],"mapped",[112]],[[120266,120266],"mapped",[113]],[[120267,120267],"mapped",[114]],[[120268,120268],"mapped",[115]],[[120269,120269],"mapped",[116]],[[120270,120270],"mapped",[117]],[[120271,120271],"mapped",[118]],[[120272,120272],"mapped",[119]],[[120273,120273],"mapped",[120]],[[120274,120274],"mapped",[121]],[[120275,120275],"mapped",[122]],[[120276,120276],"mapped",[97]],[[120277,120277],"mapped",[98]],[[120278,120278],"mapped",[99]],[[120279,120279],"mapped",[100]],[[120280,120280],"mapped",[101]],[[120281,120281],"mapped",[102]],[[120282,120282],"mapped",[103]],[[120283,120283],"mapped",[104]],[[120284,120284],"mapped",[105]],[[120285,120285],"mapped",[106]],[[120286,120286],"mapped",[107]],[[120287,120287],"mapped",[108]],[[120288,120288],"mapped",[109]],[[120289,120289],"mapped",[110]],[[120290,120290],"mapped",[111]],[[120291,120291],"mapped",[112]],[[120292,120292],"mapped",[113]],[[120293,120293],"mapped",[114]],[[120294,120294],"mapped",[115]],[[120295,120295],"mapped",[116]],[[120296,120296],"mapped",[117]],[[120297,120297],"mapped",[118]],[[120298,120298],"mapped",[119]],[[120299,120299],"mapped",[120]],[[120300,120300],"mapped",[121]],[[120301,120301],"mapped",[122]],[[120302,120302],"mapped",[97]],[[120303,120303],"mapped",[98]],[[120304,120304],"mapped",[99]],[[120305,120305],"mapped",[100]],[[120306,120306],"mapped",[101]],[[120307,120307],"mapped",[102]],[[120308,120308],"mapped",[103]],[[120309,120309],"mapped",[104]],[[120310,120310],"mapped",[105]],[[120311,120311],"mapped",[106]],[[120312,120312],"mapped",[107]],[[120313,120313],"mapped",[108]],[[120314,120314],"mapped",[109]],[[120315,120315],"mapped",[110]],[[120316,120316],"mapped",[111]],[[120317,120317],"mapped",[112]],[[120318,120318],"mapped",[113]],[[120319,120319],"mapped",[114]],[[120320,120320],"mapped",[115]],[[120321,120321],"mapped",[116]],[[120322,120322],"mapped",[117]],[[120323,120323],"mapped",[118]],[[120324,120324],"mapped",[119]],[[120325,120325],"mapped",[120]],[[120326,120326],"mapped",[121]],[[120327,120327],"mapped",[122]],[[120328,120328],"mapped",[97]],[[120329,120329],"mapped",[98]],[[120330,120330],"mapped",[99]],[[120331,120331],"mapped",[100]],[[120332,120332],"mapped",[101]],[[120333,120333],"mapped",[102]],[[120334,120334],"mapped",[103]],[[120335,120335],"mapped",[104]],[[120336,120336],"mapped",[105]],[[120337,120337],"mapped",[106]],[[120338,120338],"mapped",[107]],[[120339,120339],"mapped",[108]],[[120340,120340],"mapped",[109]],[[120341,120341],"mapped",[110]],[[120342,120342],"mapped",[111]],[[120343,120343],"mapped",[112]],[[120344,120344],"mapped",[113]],[[120345,120345],"mapped",[114]],[[120346,120346],"mapped",[115]],[[120347,120347],"mapped",[116]],[[120348,120348],"mapped",[117]],[[120349,120349],"mapped",[118]],[[120350,120350],"mapped",[119]],[[120351,120351],"mapped",[120]],[[120352,120352],"mapped",[121]],[[120353,120353],"mapped",[122]],[[120354,120354],"mapped",[97]],[[120355,120355],"mapped",[98]],[[120356,120356],"mapped",[99]],[[120357,120357],"mapped",[100]],[[120358,120358],"mapped",[101]],[[120359,120359],"mapped",[102]],[[120360,120360],"mapped",[103]],[[120361,120361],"mapped",[104]],[[120362,120362],"mapped",[105]],[[120363,120363],"mapped",[106]],[[120364,120364],"mapped",[107]],[[120365,120365],"mapped",[108]],[[120366,120366],"mapped",[109]],[[120367,120367],"mapped",[110]],[[120368,120368],"mapped",[111]],[[120369,120369],"mapped",[112]],[[120370,120370],"mapped",[113]],[[120371,120371],"mapped",[114]],[[120372,120372],"mapped",[115]],[[120373,120373],"mapped",[116]],[[120374,120374],"mapped",[117]],[[120375,120375],"mapped",[118]],[[120376,120376],"mapped",[119]],[[120377,120377],"mapped",[120]],[[120378,120378],"mapped",[121]],[[120379,120379],"mapped",[122]],[[120380,120380],"mapped",[97]],[[120381,120381],"mapped",[98]],[[120382,120382],"mapped",[99]],[[120383,120383],"mapped",[100]],[[120384,120384],"mapped",[101]],[[120385,120385],"mapped",[102]],[[120386,120386],"mapped",[103]],[[120387,120387],"mapped",[104]],[[120388,120388],"mapped",[105]],[[120389,120389],"mapped",[106]],[[120390,120390],"mapped",[107]],[[120391,120391],"mapped",[108]],[[120392,120392],"mapped",[109]],[[120393,120393],"mapped",[110]],[[120394,120394],"mapped",[111]],[[120395,120395],"mapped",[112]],[[120396,120396],"mapped",[113]],[[120397,120397],"mapped",[114]],[[120398,120398],"mapped",[115]],[[120399,120399],"mapped",[116]],[[120400,120400],"mapped",[117]],[[120401,120401],"mapped",[118]],[[120402,120402],"mapped",[119]],[[120403,120403],"mapped",[120]],[[120404,120404],"mapped",[121]],[[120405,120405],"mapped",[122]],[[120406,120406],"mapped",[97]],[[120407,120407],"mapped",[98]],[[120408,120408],"mapped",[99]],[[120409,120409],"mapped",[100]],[[120410,120410],"mapped",[101]],[[120411,120411],"mapped",[102]],[[120412,120412],"mapped",[103]],[[120413,120413],"mapped",[104]],[[120414,120414],"mapped",[105]],[[120415,120415],"mapped",[106]],[[120416,120416],"mapped",[107]],[[120417,120417],"mapped",[108]],[[120418,120418],"mapped",[109]],[[120419,120419],"mapped",[110]],[[120420,120420],"mapped",[111]],[[120421,120421],"mapped",[112]],[[120422,120422],"mapped",[113]],[[120423,120423],"mapped",[114]],[[120424,120424],"mapped",[115]],[[120425,120425],"mapped",[116]],[[120426,120426],"mapped",[117]],[[120427,120427],"mapped",[118]],[[120428,120428],"mapped",[119]],[[120429,120429],"mapped",[120]],[[120430,120430],"mapped",[121]],[[120431,120431],"mapped",[122]],[[120432,120432],"mapped",[97]],[[120433,120433],"mapped",[98]],[[120434,120434],"mapped",[99]],[[120435,120435],"mapped",[100]],[[120436,120436],"mapped",[101]],[[120437,120437],"mapped",[102]],[[120438,120438],"mapped",[103]],[[120439,120439],"mapped",[104]],[[120440,120440],"mapped",[105]],[[120441,120441],"mapped",[106]],[[120442,120442],"mapped",[107]],[[120443,120443],"mapped",[108]],[[120444,120444],"mapped",[109]],[[120445,120445],"mapped",[110]],[[120446,120446],"mapped",[111]],[[120447,120447],"mapped",[112]],[[120448,120448],"mapped",[113]],[[120449,120449],"mapped",[114]],[[120450,120450],"mapped",[115]],[[120451,120451],"mapped",[116]],[[120452,120452],"mapped",[117]],[[120453,120453],"mapped",[118]],[[120454,120454],"mapped",[119]],[[120455,120455],"mapped",[120]],[[120456,120456],"mapped",[121]],[[120457,120457],"mapped",[122]],[[120458,120458],"mapped",[97]],[[120459,120459],"mapped",[98]],[[120460,120460],"mapped",[99]],[[120461,120461],"mapped",[100]],[[120462,120462],"mapped",[101]],[[120463,120463],"mapped",[102]],[[120464,120464],"mapped",[103]],[[120465,120465],"mapped",[104]],[[120466,120466],"mapped",[105]],[[120467,120467],"mapped",[106]],[[120468,120468],"mapped",[107]],[[120469,120469],"mapped",[108]],[[120470,120470],"mapped",[109]],[[120471,120471],"mapped",[110]],[[120472,120472],"mapped",[111]],[[120473,120473],"mapped",[112]],[[120474,120474],"mapped",[113]],[[120475,120475],"mapped",[114]],[[120476,120476],"mapped",[115]],[[120477,120477],"mapped",[116]],[[120478,120478],"mapped",[117]],[[120479,120479],"mapped",[118]],[[120480,120480],"mapped",[119]],[[120481,120481],"mapped",[120]],[[120482,120482],"mapped",[121]],[[120483,120483],"mapped",[122]],[[120484,120484],"mapped",[305]],[[120485,120485],"mapped",[567]],[[120486,120487],"disallowed"],[[120488,120488],"mapped",[945]],[[120489,120489],"mapped",[946]],[[120490,120490],"mapped",[947]],[[120491,120491],"mapped",[948]],[[120492,120492],"mapped",[949]],[[120493,120493],"mapped",[950]],[[120494,120494],"mapped",[951]],[[120495,120495],"mapped",[952]],[[120496,120496],"mapped",[953]],[[120497,120497],"mapped",[954]],[[120498,120498],"mapped",[955]],[[120499,120499],"mapped",[956]],[[120500,120500],"mapped",[957]],[[120501,120501],"mapped",[958]],[[120502,120502],"mapped",[959]],[[120503,120503],"mapped",[960]],[[120504,120504],"mapped",[961]],[[120505,120505],"mapped",[952]],[[120506,120506],"mapped",[963]],[[120507,120507],"mapped",[964]],[[120508,120508],"mapped",[965]],[[120509,120509],"mapped",[966]],[[120510,120510],"mapped",[967]],[[120511,120511],"mapped",[968]],[[120512,120512],"mapped",[969]],[[120513,120513],"mapped",[8711]],[[120514,120514],"mapped",[945]],[[120515,120515],"mapped",[946]],[[120516,120516],"mapped",[947]],[[120517,120517],"mapped",[948]],[[120518,120518],"mapped",[949]],[[120519,120519],"mapped",[950]],[[120520,120520],"mapped",[951]],[[120521,120521],"mapped",[952]],[[120522,120522],"mapped",[953]],[[120523,120523],"mapped",[954]],[[120524,120524],"mapped",[955]],[[120525,120525],"mapped",[956]],[[120526,120526],"mapped",[957]],[[120527,120527],"mapped",[958]],[[120528,120528],"mapped",[959]],[[120529,120529],"mapped",[960]],[[120530,120530],"mapped",[961]],[[120531,120532],"mapped",[963]],[[120533,120533],"mapped",[964]],[[120534,120534],"mapped",[965]],[[120535,120535],"mapped",[966]],[[120536,120536],"mapped",[967]],[[120537,120537],"mapped",[968]],[[120538,120538],"mapped",[969]],[[120539,120539],"mapped",[8706]],[[120540,120540],"mapped",[949]],[[120541,120541],"mapped",[952]],[[120542,120542],"mapped",[954]],[[120543,120543],"mapped",[966]],[[120544,120544],"mapped",[961]],[[120545,120545],"mapped",[960]],[[120546,120546],"mapped",[945]],[[120547,120547],"mapped",[946]],[[120548,120548],"mapped",[947]],[[120549,120549],"mapped",[948]],[[120550,120550],"mapped",[949]],[[120551,120551],"mapped",[950]],[[120552,120552],"mapped",[951]],[[120553,120553],"mapped",[952]],[[120554,120554],"mapped",[953]],[[120555,120555],"mapped",[954]],[[120556,120556],"mapped",[955]],[[120557,120557],"mapped",[956]],[[120558,120558],"mapped",[957]],[[120559,120559],"mapped",[958]],[[120560,120560],"mapped",[959]],[[120561,120561],"mapped",[960]],[[120562,120562],"mapped",[961]],[[120563,120563],"mapped",[952]],[[120564,120564],"mapped",[963]],[[120565,120565],"mapped",[964]],[[120566,120566],"mapped",[965]],[[120567,120567],"mapped",[966]],[[120568,120568],"mapped",[967]],[[120569,120569],"mapped",[968]],[[120570,120570],"mapped",[969]],[[120571,120571],"mapped",[8711]],[[120572,120572],"mapped",[945]],[[120573,120573],"mapped",[946]],[[120574,120574],"mapped",[947]],[[120575,120575],"mapped",[948]],[[120576,120576],"mapped",[949]],[[120577,120577],"mapped",[950]],[[120578,120578],"mapped",[951]],[[120579,120579],"mapped",[952]],[[120580,120580],"mapped",[953]],[[120581,120581],"mapped",[954]],[[120582,120582],"mapped",[955]],[[120583,120583],"mapped",[956]],[[120584,120584],"mapped",[957]],[[120585,120585],"mapped",[958]],[[120586,120586],"mapped",[959]],[[120587,120587],"mapped",[960]],[[120588,120588],"mapped",[961]],[[120589,120590],"mapped",[963]],[[120591,120591],"mapped",[964]],[[120592,120592],"mapped",[965]],[[120593,120593],"mapped",[966]],[[120594,120594],"mapped",[967]],[[120595,120595],"mapped",[968]],[[120596,120596],"mapped",[969]],[[120597,120597],"mapped",[8706]],[[120598,120598],"mapped",[949]],[[120599,120599],"mapped",[952]],[[120600,120600],"mapped",[954]],[[120601,120601],"mapped",[966]],[[120602,120602],"mapped",[961]],[[120603,120603],"mapped",[960]],[[120604,120604],"mapped",[945]],[[120605,120605],"mapped",[946]],[[120606,120606],"mapped",[947]],[[120607,120607],"mapped",[948]],[[120608,120608],"mapped",[949]],[[120609,120609],"mapped",[950]],[[120610,120610],"mapped",[951]],[[120611,120611],"mapped",[952]],[[120612,120612],"mapped",[953]],[[120613,120613],"mapped",[954]],[[120614,120614],"mapped",[955]],[[120615,120615],"mapped",[956]],[[120616,120616],"mapped",[957]],[[120617,120617],"mapped",[958]],[[120618,120618],"mapped",[959]],[[120619,120619],"mapped",[960]],[[120620,120620],"mapped",[961]],[[120621,120621],"mapped",[952]],[[120622,120622],"mapped",[963]],[[120623,120623],"mapped",[964]],[[120624,120624],"mapped",[965]],[[120625,120625],"mapped",[966]],[[120626,120626],"mapped",[967]],[[120627,120627],"mapped",[968]],[[120628,120628],"mapped",[969]],[[120629,120629],"mapped",[8711]],[[120630,120630],"mapped",[945]],[[120631,120631],"mapped",[946]],[[120632,120632],"mapped",[947]],[[120633,120633],"mapped",[948]],[[120634,120634],"mapped",[949]],[[120635,120635],"mapped",[950]],[[120636,120636],"mapped",[951]],[[120637,120637],"mapped",[952]],[[120638,120638],"mapped",[953]],[[120639,120639],"mapped",[954]],[[120640,120640],"mapped",[955]],[[120641,120641],"mapped",[956]],[[120642,120642],"mapped",[957]],[[120643,120643],"mapped",[958]],[[120644,120644],"mapped",[959]],[[120645,120645],"mapped",[960]],[[120646,120646],"mapped",[961]],[[120647,120648],"mapped",[963]],[[120649,120649],"mapped",[964]],[[120650,120650],"mapped",[965]],[[120651,120651],"mapped",[966]],[[120652,120652],"mapped",[967]],[[120653,120653],"mapped",[968]],[[120654,120654],"mapped",[969]],[[120655,120655],"mapped",[8706]],[[120656,120656],"mapped",[949]],[[120657,120657],"mapped",[952]],[[120658,120658],"mapped",[954]],[[120659,120659],"mapped",[966]],[[120660,120660],"mapped",[961]],[[120661,120661],"mapped",[960]],[[120662,120662],"mapped",[945]],[[120663,120663],"mapped",[946]],[[120664,120664],"mapped",[947]],[[120665,120665],"mapped",[948]],[[120666,120666],"mapped",[949]],[[120667,120667],"mapped",[950]],[[120668,120668],"mapped",[951]],[[120669,120669],"mapped",[952]],[[120670,120670],"mapped",[953]],[[120671,120671],"mapped",[954]],[[120672,120672],"mapped",[955]],[[120673,120673],"mapped",[956]],[[120674,120674],"mapped",[957]],[[120675,120675],"mapped",[958]],[[120676,120676],"mapped",[959]],[[120677,120677],"mapped",[960]],[[120678,120678],"mapped",[961]],[[120679,120679],"mapped",[952]],[[120680,120680],"mapped",[963]],[[120681,120681],"mapped",[964]],[[120682,120682],"mapped",[965]],[[120683,120683],"mapped",[966]],[[120684,120684],"mapped",[967]],[[120685,120685],"mapped",[968]],[[120686,120686],"mapped",[969]],[[120687,120687],"mapped",[8711]],[[120688,120688],"mapped",[945]],[[120689,120689],"mapped",[946]],[[120690,120690],"mapped",[947]],[[120691,120691],"mapped",[948]],[[120692,120692],"mapped",[949]],[[120693,120693],"mapped",[950]],[[120694,120694],"mapped",[951]],[[120695,120695],"mapped",[952]],[[120696,120696],"mapped",[953]],[[120697,120697],"mapped",[954]],[[120698,120698],"mapped",[955]],[[120699,120699],"mapped",[956]],[[120700,120700],"mapped",[957]],[[120701,120701],"mapped",[958]],[[120702,120702],"mapped",[959]],[[120703,120703],"mapped",[960]],[[120704,120704],"mapped",[961]],[[120705,120706],"mapped",[963]],[[120707,120707],"mapped",[964]],[[120708,120708],"mapped",[965]],[[120709,120709],"mapped",[966]],[[120710,120710],"mapped",[967]],[[120711,120711],"mapped",[968]],[[120712,120712],"mapped",[969]],[[120713,120713],"mapped",[8706]],[[120714,120714],"mapped",[949]],[[120715,120715],"mapped",[952]],[[120716,120716],"mapped",[954]],[[120717,120717],"mapped",[966]],[[120718,120718],"mapped",[961]],[[120719,120719],"mapped",[960]],[[120720,120720],"mapped",[945]],[[120721,120721],"mapped",[946]],[[120722,120722],"mapped",[947]],[[120723,120723],"mapped",[948]],[[120724,120724],"mapped",[949]],[[120725,120725],"mapped",[950]],[[120726,120726],"mapped",[951]],[[120727,120727],"mapped",[952]],[[120728,120728],"mapped",[953]],[[120729,120729],"mapped",[954]],[[120730,120730],"mapped",[955]],[[120731,120731],"mapped",[956]],[[120732,120732],"mapped",[957]],[[120733,120733],"mapped",[958]],[[120734,120734],"mapped",[959]],[[120735,120735],"mapped",[960]],[[120736,120736],"mapped",[961]],[[120737,120737],"mapped",[952]],[[120738,120738],"mapped",[963]],[[120739,120739],"mapped",[964]],[[120740,120740],"mapped",[965]],[[120741,120741],"mapped",[966]],[[120742,120742],"mapped",[967]],[[120743,120743],"mapped",[968]],[[120744,120744],"mapped",[969]],[[120745,120745],"mapped",[8711]],[[120746,120746],"mapped",[945]],[[120747,120747],"mapped",[946]],[[120748,120748],"mapped",[947]],[[120749,120749],"mapped",[948]],[[120750,120750],"mapped",[949]],[[120751,120751],"mapped",[950]],[[120752,120752],"mapped",[951]],[[120753,120753],"mapped",[952]],[[120754,120754],"mapped",[953]],[[120755,120755],"mapped",[954]],[[120756,120756],"mapped",[955]],[[120757,120757],"mapped",[956]],[[120758,120758],"mapped",[957]],[[120759,120759],"mapped",[958]],[[120760,120760],"mapped",[959]],[[120761,120761],"mapped",[960]],[[120762,120762],"mapped",[961]],[[120763,120764],"mapped",[963]],[[120765,120765],"mapped",[964]],[[120766,120766],"mapped",[965]],[[120767,120767],"mapped",[966]],[[120768,120768],"mapped",[967]],[[120769,120769],"mapped",[968]],[[120770,120770],"mapped",[969]],[[120771,120771],"mapped",[8706]],[[120772,120772],"mapped",[949]],[[120773,120773],"mapped",[952]],[[120774,120774],"mapped",[954]],[[120775,120775],"mapped",[966]],[[120776,120776],"mapped",[961]],[[120777,120777],"mapped",[960]],[[120778,120779],"mapped",[989]],[[120780,120781],"disallowed"],[[120782,120782],"mapped",[48]],[[120783,120783],"mapped",[49]],[[120784,120784],"mapped",[50]],[[120785,120785],"mapped",[51]],[[120786,120786],"mapped",[52]],[[120787,120787],"mapped",[53]],[[120788,120788],"mapped",[54]],[[120789,120789],"mapped",[55]],[[120790,120790],"mapped",[56]],[[120791,120791],"mapped",[57]],[[120792,120792],"mapped",[48]],[[120793,120793],"mapped",[49]],[[120794,120794],"mapped",[50]],[[120795,120795],"mapped",[51]],[[120796,120796],"mapped",[52]],[[120797,120797],"mapped",[53]],[[120798,120798],"mapped",[54]],[[120799,120799],"mapped",[55]],[[120800,120800],"mapped",[56]],[[120801,120801],"mapped",[57]],[[120802,120802],"mapped",[48]],[[120803,120803],"mapped",[49]],[[120804,120804],"mapped",[50]],[[120805,120805],"mapped",[51]],[[120806,120806],"mapped",[52]],[[120807,120807],"mapped",[53]],[[120808,120808],"mapped",[54]],[[120809,120809],"mapped",[55]],[[120810,120810],"mapped",[56]],[[120811,120811],"mapped",[57]],[[120812,120812],"mapped",[48]],[[120813,120813],"mapped",[49]],[[120814,120814],"mapped",[50]],[[120815,120815],"mapped",[51]],[[120816,120816],"mapped",[52]],[[120817,120817],"mapped",[53]],[[120818,120818],"mapped",[54]],[[120819,120819],"mapped",[55]],[[120820,120820],"mapped",[56]],[[120821,120821],"mapped",[57]],[[120822,120822],"mapped",[48]],[[120823,120823],"mapped",[49]],[[120824,120824],"mapped",[50]],[[120825,120825],"mapped",[51]],[[120826,120826],"mapped",[52]],[[120827,120827],"mapped",[53]],[[120828,120828],"mapped",[54]],[[120829,120829],"mapped",[55]],[[120830,120830],"mapped",[56]],[[120831,120831],"mapped",[57]],[[120832,121343],"valid",[],"NV8"],[[121344,121398],"valid"],[[121399,121402],"valid",[],"NV8"],[[121403,121452],"valid"],[[121453,121460],"valid",[],"NV8"],[[121461,121461],"valid"],[[121462,121475],"valid",[],"NV8"],[[121476,121476],"valid"],[[121477,121483],"valid",[],"NV8"],[[121484,121498],"disallowed"],[[121499,121503],"valid"],[[121504,121504],"disallowed"],[[121505,121519],"valid"],[[121520,124927],"disallowed"],[[124928,125124],"valid"],[[125125,125126],"disallowed"],[[125127,125135],"valid",[],"NV8"],[[125136,125142],"valid"],[[125143,126463],"disallowed"],[[126464,126464],"mapped",[1575]],[[126465,126465],"mapped",[1576]],[[126466,126466],"mapped",[1580]],[[126467,126467],"mapped",[1583]],[[126468,126468],"disallowed"],[[126469,126469],"mapped",[1608]],[[126470,126470],"mapped",[1586]],[[126471,126471],"mapped",[1581]],[[126472,126472],"mapped",[1591]],[[126473,126473],"mapped",[1610]],[[126474,126474],"mapped",[1603]],[[126475,126475],"mapped",[1604]],[[126476,126476],"mapped",[1605]],[[126477,126477],"mapped",[1606]],[[126478,126478],"mapped",[1587]],[[126479,126479],"mapped",[1593]],[[126480,126480],"mapped",[1601]],[[126481,126481],"mapped",[1589]],[[126482,126482],"mapped",[1602]],[[126483,126483],"mapped",[1585]],[[126484,126484],"mapped",[1588]],[[126485,126485],"mapped",[1578]],[[126486,126486],"mapped",[1579]],[[126487,126487],"mapped",[1582]],[[126488,126488],"mapped",[1584]],[[126489,126489],"mapped",[1590]],[[126490,126490],"mapped",[1592]],[[126491,126491],"mapped",[1594]],[[126492,126492],"mapped",[1646]],[[126493,126493],"mapped",[1722]],[[126494,126494],"mapped",[1697]],[[126495,126495],"mapped",[1647]],[[126496,126496],"disallowed"],[[126497,126497],"mapped",[1576]],[[126498,126498],"mapped",[1580]],[[126499,126499],"disallowed"],[[126500,126500],"mapped",[1607]],[[126501,126502],"disallowed"],[[126503,126503],"mapped",[1581]],[[126504,126504],"disallowed"],[[126505,126505],"mapped",[1610]],[[126506,126506],"mapped",[1603]],[[126507,126507],"mapped",[1604]],[[126508,126508],"mapped",[1605]],[[126509,126509],"mapped",[1606]],[[126510,126510],"mapped",[1587]],[[126511,126511],"mapped",[1593]],[[126512,126512],"mapped",[1601]],[[126513,126513],"mapped",[1589]],[[126514,126514],"mapped",[1602]],[[126515,126515],"disallowed"],[[126516,126516],"mapped",[1588]],[[126517,126517],"mapped",[1578]],[[126518,126518],"mapped",[1579]],[[126519,126519],"mapped",[1582]],[[126520,126520],"disallowed"],[[126521,126521],"mapped",[1590]],[[126522,126522],"disallowed"],[[126523,126523],"mapped",[1594]],[[126524,126529],"disallowed"],[[126530,126530],"mapped",[1580]],[[126531,126534],"disallowed"],[[126535,126535],"mapped",[1581]],[[126536,126536],"disallowed"],[[126537,126537],"mapped",[1610]],[[126538,126538],"disallowed"],[[126539,126539],"mapped",[1604]],[[126540,126540],"disallowed"],[[126541,126541],"mapped",[1606]],[[126542,126542],"mapped",[1587]],[[126543,126543],"mapped",[1593]],[[126544,126544],"disallowed"],[[126545,126545],"mapped",[1589]],[[126546,126546],"mapped",[1602]],[[126547,126547],"disallowed"],[[126548,126548],"mapped",[1588]],[[126549,126550],"disallowed"],[[126551,126551],"mapped",[1582]],[[126552,126552],"disallowed"],[[126553,126553],"mapped",[1590]],[[126554,126554],"disallowed"],[[126555,126555],"mapped",[1594]],[[126556,126556],"disallowed"],[[126557,126557],"mapped",[1722]],[[126558,126558],"disallowed"],[[126559,126559],"mapped",[1647]],[[126560,126560],"disallowed"],[[126561,126561],"mapped",[1576]],[[126562,126562],"mapped",[1580]],[[126563,126563],"disallowed"],[[126564,126564],"mapped",[1607]],[[126565,126566],"disallowed"],[[126567,126567],"mapped",[1581]],[[126568,126568],"mapped",[1591]],[[126569,126569],"mapped",[1610]],[[126570,126570],"mapped",[1603]],[[126571,126571],"disallowed"],[[126572,126572],"mapped",[1605]],[[126573,126573],"mapped",[1606]],[[126574,126574],"mapped",[1587]],[[126575,126575],"mapped",[1593]],[[126576,126576],"mapped",[1601]],[[126577,126577],"mapped",[1589]],[[126578,126578],"mapped",[1602]],[[126579,126579],"disallowed"],[[126580,126580],"mapped",[1588]],[[126581,126581],"mapped",[1578]],[[126582,126582],"mapped",[1579]],[[126583,126583],"mapped",[1582]],[[126584,126584],"disallowed"],[[126585,126585],"mapped",[1590]],[[126586,126586],"mapped",[1592]],[[126587,126587],"mapped",[1594]],[[126588,126588],"mapped",[1646]],[[126589,126589],"disallowed"],[[126590,126590],"mapped",[1697]],[[126591,126591],"disallowed"],[[126592,126592],"mapped",[1575]],[[126593,126593],"mapped",[1576]],[[126594,126594],"mapped",[1580]],[[126595,126595],"mapped",[1583]],[[126596,126596],"mapped",[1607]],[[126597,126597],"mapped",[1608]],[[126598,126598],"mapped",[1586]],[[126599,126599],"mapped",[1581]],[[126600,126600],"mapped",[1591]],[[126601,126601],"mapped",[1610]],[[126602,126602],"disallowed"],[[126603,126603],"mapped",[1604]],[[126604,126604],"mapped",[1605]],[[126605,126605],"mapped",[1606]],[[126606,126606],"mapped",[1587]],[[126607,126607],"mapped",[1593]],[[126608,126608],"mapped",[1601]],[[126609,126609],"mapped",[1589]],[[126610,126610],"mapped",[1602]],[[126611,126611],"mapped",[1585]],[[126612,126612],"mapped",[1588]],[[126613,126613],"mapped",[1578]],[[126614,126614],"mapped",[1579]],[[126615,126615],"mapped",[1582]],[[126616,126616],"mapped",[1584]],[[126617,126617],"mapped",[1590]],[[126618,126618],"mapped",[1592]],[[126619,126619],"mapped",[1594]],[[126620,126624],"disallowed"],[[126625,126625],"mapped",[1576]],[[126626,126626],"mapped",[1580]],[[126627,126627],"mapped",[1583]],[[126628,126628],"disallowed"],[[126629,126629],"mapped",[1608]],[[126630,126630],"mapped",[1586]],[[126631,126631],"mapped",[1581]],[[126632,126632],"mapped",[1591]],[[126633,126633],"mapped",[1610]],[[126634,126634],"disallowed"],[[126635,126635],"mapped",[1604]],[[126636,126636],"mapped",[1605]],[[126637,126637],"mapped",[1606]],[[126638,126638],"mapped",[1587]],[[126639,126639],"mapped",[1593]],[[126640,126640],"mapped",[1601]],[[126641,126641],"mapped",[1589]],[[126642,126642],"mapped",[1602]],[[126643,126643],"mapped",[1585]],[[126644,126644],"mapped",[1588]],[[126645,126645],"mapped",[1578]],[[126646,126646],"mapped",[1579]],[[126647,126647],"mapped",[1582]],[[126648,126648],"mapped",[1584]],[[126649,126649],"mapped",[1590]],[[126650,126650],"mapped",[1592]],[[126651,126651],"mapped",[1594]],[[126652,126703],"disallowed"],[[126704,126705],"valid",[],"NV8"],[[126706,126975],"disallowed"],[[126976,127019],"valid",[],"NV8"],[[127020,127023],"disallowed"],[[127024,127123],"valid",[],"NV8"],[[127124,127135],"disallowed"],[[127136,127150],"valid",[],"NV8"],[[127151,127152],"disallowed"],[[127153,127166],"valid",[],"NV8"],[[127167,127167],"valid",[],"NV8"],[[127168,127168],"disallowed"],[[127169,127183],"valid",[],"NV8"],[[127184,127184],"disallowed"],[[127185,127199],"valid",[],"NV8"],[[127200,127221],"valid",[],"NV8"],[[127222,127231],"disallowed"],[[127232,127232],"disallowed"],[[127233,127233],"disallowed_STD3_mapped",[48,44]],[[127234,127234],"disallowed_STD3_mapped",[49,44]],[[127235,127235],"disallowed_STD3_mapped",[50,44]],[[127236,127236],"disallowed_STD3_mapped",[51,44]],[[127237,127237],"disallowed_STD3_mapped",[52,44]],[[127238,127238],"disallowed_STD3_mapped",[53,44]],[[127239,127239],"disallowed_STD3_mapped",[54,44]],[[127240,127240],"disallowed_STD3_mapped",[55,44]],[[127241,127241],"disallowed_STD3_mapped",[56,44]],[[127242,127242],"disallowed_STD3_mapped",[57,44]],[[127243,127244],"valid",[],"NV8"],[[127245,127247],"disallowed"],[[127248,127248],"disallowed_STD3_mapped",[40,97,41]],[[127249,127249],"disallowed_STD3_mapped",[40,98,41]],[[127250,127250],"disallowed_STD3_mapped",[40,99,41]],[[127251,127251],"disallowed_STD3_mapped",[40,100,41]],[[127252,127252],"disallowed_STD3_mapped",[40,101,41]],[[127253,127253],"disallowed_STD3_mapped",[40,102,41]],[[127254,127254],"disallowed_STD3_mapped",[40,103,41]],[[127255,127255],"disallowed_STD3_mapped",[40,104,41]],[[127256,127256],"disallowed_STD3_mapped",[40,105,41]],[[127257,127257],"disallowed_STD3_mapped",[40,106,41]],[[127258,127258],"disallowed_STD3_mapped",[40,107,41]],[[127259,127259],"disallowed_STD3_mapped",[40,108,41]],[[127260,127260],"disallowed_STD3_mapped",[40,109,41]],[[127261,127261],"disallowed_STD3_mapped",[40,110,41]],[[127262,127262],"disallowed_STD3_mapped",[40,111,41]],[[127263,127263],"disallowed_STD3_mapped",[40,112,41]],[[127264,127264],"disallowed_STD3_mapped",[40,113,41]],[[127265,127265],"disallowed_STD3_mapped",[40,114,41]],[[127266,127266],"disallowed_STD3_mapped",[40,115,41]],[[127267,127267],"disallowed_STD3_mapped",[40,116,41]],[[127268,127268],"disallowed_STD3_mapped",[40,117,41]],[[127269,127269],"disallowed_STD3_mapped",[40,118,41]],[[127270,127270],"disallowed_STD3_mapped",[40,119,41]],[[127271,127271],"disallowed_STD3_mapped",[40,120,41]],[[127272,127272],"disallowed_STD3_mapped",[40,121,41]],[[127273,127273],"disallowed_STD3_mapped",[40,122,41]],[[127274,127274],"mapped",[12308,115,12309]],[[127275,127275],"mapped",[99]],[[127276,127276],"mapped",[114]],[[127277,127277],"mapped",[99,100]],[[127278,127278],"mapped",[119,122]],[[127279,127279],"disallowed"],[[127280,127280],"mapped",[97]],[[127281,127281],"mapped",[98]],[[127282,127282],"mapped",[99]],[[127283,127283],"mapped",[100]],[[127284,127284],"mapped",[101]],[[127285,127285],"mapped",[102]],[[127286,127286],"mapped",[103]],[[127287,127287],"mapped",[104]],[[127288,127288],"mapped",[105]],[[127289,127289],"mapped",[106]],[[127290,127290],"mapped",[107]],[[127291,127291],"mapped",[108]],[[127292,127292],"mapped",[109]],[[127293,127293],"mapped",[110]],[[127294,127294],"mapped",[111]],[[127295,127295],"mapped",[112]],[[127296,127296],"mapped",[113]],[[127297,127297],"mapped",[114]],[[127298,127298],"mapped",[115]],[[127299,127299],"mapped",[116]],[[127300,127300],"mapped",[117]],[[127301,127301],"mapped",[118]],[[127302,127302],"mapped",[119]],[[127303,127303],"mapped",[120]],[[127304,127304],"mapped",[121]],[[127305,127305],"mapped",[122]],[[127306,127306],"mapped",[104,118]],[[127307,127307],"mapped",[109,118]],[[127308,127308],"mapped",[115,100]],[[127309,127309],"mapped",[115,115]],[[127310,127310],"mapped",[112,112,118]],[[127311,127311],"mapped",[119,99]],[[127312,127318],"valid",[],"NV8"],[[127319,127319],"valid",[],"NV8"],[[127320,127326],"valid",[],"NV8"],[[127327,127327],"valid",[],"NV8"],[[127328,127337],"valid",[],"NV8"],[[127338,127338],"mapped",[109,99]],[[127339,127339],"mapped",[109,100]],[[127340,127343],"disallowed"],[[127344,127352],"valid",[],"NV8"],[[127353,127353],"valid",[],"NV8"],[[127354,127354],"valid",[],"NV8"],[[127355,127356],"valid",[],"NV8"],[[127357,127358],"valid",[],"NV8"],[[127359,127359],"valid",[],"NV8"],[[127360,127369],"valid",[],"NV8"],[[127370,127373],"valid",[],"NV8"],[[127374,127375],"valid",[],"NV8"],[[127376,127376],"mapped",[100,106]],[[127377,127386],"valid",[],"NV8"],[[127387,127461],"disallowed"],[[127462,127487],"valid",[],"NV8"],[[127488,127488],"mapped",[12411,12363]],[[127489,127489],"mapped",[12467,12467]],[[127490,127490],"mapped",[12469]],[[127491,127503],"disallowed"],[[127504,127504],"mapped",[25163]],[[127505,127505],"mapped",[23383]],[[127506,127506],"mapped",[21452]],[[127507,127507],"mapped",[12487]],[[127508,127508],"mapped",[20108]],[[127509,127509],"mapped",[22810]],[[127510,127510],"mapped",[35299]],[[127511,127511],"mapped",[22825]],[[127512,127512],"mapped",[20132]],[[127513,127513],"mapped",[26144]],[[127514,127514],"mapped",[28961]],[[127515,127515],"mapped",[26009]],[[127516,127516],"mapped",[21069]],[[127517,127517],"mapped",[24460]],[[127518,127518],"mapped",[20877]],[[127519,127519],"mapped",[26032]],[[127520,127520],"mapped",[21021]],[[127521,127521],"mapped",[32066]],[[127522,127522],"mapped",[29983]],[[127523,127523],"mapped",[36009]],[[127524,127524],"mapped",[22768]],[[127525,127525],"mapped",[21561]],[[127526,127526],"mapped",[28436]],[[127527,127527],"mapped",[25237]],[[127528,127528],"mapped",[25429]],[[127529,127529],"mapped",[19968]],[[127530,127530],"mapped",[19977]],[[127531,127531],"mapped",[36938]],[[127532,127532],"mapped",[24038]],[[127533,127533],"mapped",[20013]],[[127534,127534],"mapped",[21491]],[[127535,127535],"mapped",[25351]],[[127536,127536],"mapped",[36208]],[[127537,127537],"mapped",[25171]],[[127538,127538],"mapped",[31105]],[[127539,127539],"mapped",[31354]],[[127540,127540],"mapped",[21512]],[[127541,127541],"mapped",[28288]],[[127542,127542],"mapped",[26377]],[[127543,127543],"mapped",[26376]],[[127544,127544],"mapped",[30003]],[[127545,127545],"mapped",[21106]],[[127546,127546],"mapped",[21942]],[[127547,127551],"disallowed"],[[127552,127552],"mapped",[12308,26412,12309]],[[127553,127553],"mapped",[12308,19977,12309]],[[127554,127554],"mapped",[12308,20108,12309]],[[127555,127555],"mapped",[12308,23433,12309]],[[127556,127556],"mapped",[12308,28857,12309]],[[127557,127557],"mapped",[12308,25171,12309]],[[127558,127558],"mapped",[12308,30423,12309]],[[127559,127559],"mapped",[12308,21213,12309]],[[127560,127560],"mapped",[12308,25943,12309]],[[127561,127567],"disallowed"],[[127568,127568],"mapped",[24471]],[[127569,127569],"mapped",[21487]],[[127570,127743],"disallowed"],[[127744,127776],"valid",[],"NV8"],[[127777,127788],"valid",[],"NV8"],[[127789,127791],"valid",[],"NV8"],[[127792,127797],"valid",[],"NV8"],[[127798,127798],"valid",[],"NV8"],[[127799,127868],"valid",[],"NV8"],[[127869,127869],"valid",[],"NV8"],[[127870,127871],"valid",[],"NV8"],[[127872,127891],"valid",[],"NV8"],[[127892,127903],"valid",[],"NV8"],[[127904,127940],"valid",[],"NV8"],[[127941,127941],"valid",[],"NV8"],[[127942,127946],"valid",[],"NV8"],[[127947,127950],"valid",[],"NV8"],[[127951,127955],"valid",[],"NV8"],[[127956,127967],"valid",[],"NV8"],[[127968,127984],"valid",[],"NV8"],[[127985,127991],"valid",[],"NV8"],[[127992,127999],"valid",[],"NV8"],[[128000,128062],"valid",[],"NV8"],[[128063,128063],"valid",[],"NV8"],[[128064,128064],"valid",[],"NV8"],[[128065,128065],"valid",[],"NV8"],[[128066,128247],"valid",[],"NV8"],[[128248,128248],"valid",[],"NV8"],[[128249,128252],"valid",[],"NV8"],[[128253,128254],"valid",[],"NV8"],[[128255,128255],"valid",[],"NV8"],[[128256,128317],"valid",[],"NV8"],[[128318,128319],"valid",[],"NV8"],[[128320,128323],"valid",[],"NV8"],[[128324,128330],"valid",[],"NV8"],[[128331,128335],"valid",[],"NV8"],[[128336,128359],"valid",[],"NV8"],[[128360,128377],"valid",[],"NV8"],[[128378,128378],"disallowed"],[[128379,128419],"valid",[],"NV8"],[[128420,128420],"disallowed"],[[128421,128506],"valid",[],"NV8"],[[128507,128511],"valid",[],"NV8"],[[128512,128512],"valid",[],"NV8"],[[128513,128528],"valid",[],"NV8"],[[128529,128529],"valid",[],"NV8"],[[128530,128532],"valid",[],"NV8"],[[128533,128533],"valid",[],"NV8"],[[128534,128534],"valid",[],"NV8"],[[128535,128535],"valid",[],"NV8"],[[128536,128536],"valid",[],"NV8"],[[128537,128537],"valid",[],"NV8"],[[128538,128538],"valid",[],"NV8"],[[128539,128539],"valid",[],"NV8"],[[128540,128542],"valid",[],"NV8"],[[128543,128543],"valid",[],"NV8"],[[128544,128549],"valid",[],"NV8"],[[128550,128551],"valid",[],"NV8"],[[128552,128555],"valid",[],"NV8"],[[128556,128556],"valid",[],"NV8"],[[128557,128557],"valid",[],"NV8"],[[128558,128559],"valid",[],"NV8"],[[128560,128563],"valid",[],"NV8"],[[128564,128564],"valid",[],"NV8"],[[128565,128576],"valid",[],"NV8"],[[128577,128578],"valid",[],"NV8"],[[128579,128580],"valid",[],"NV8"],[[128581,128591],"valid",[],"NV8"],[[128592,128639],"valid",[],"NV8"],[[128640,128709],"valid",[],"NV8"],[[128710,128719],"valid",[],"NV8"],[[128720,128720],"valid",[],"NV8"],[[128721,128735],"disallowed"],[[128736,128748],"valid",[],"NV8"],[[128749,128751],"disallowed"],[[128752,128755],"valid",[],"NV8"],[[128756,128767],"disallowed"],[[128768,128883],"valid",[],"NV8"],[[128884,128895],"disallowed"],[[128896,128980],"valid",[],"NV8"],[[128981,129023],"disallowed"],[[129024,129035],"valid",[],"NV8"],[[129036,129039],"disallowed"],[[129040,129095],"valid",[],"NV8"],[[129096,129103],"disallowed"],[[129104,129113],"valid",[],"NV8"],[[129114,129119],"disallowed"],[[129120,129159],"valid",[],"NV8"],[[129160,129167],"disallowed"],[[129168,129197],"valid",[],"NV8"],[[129198,129295],"disallowed"],[[129296,129304],"valid",[],"NV8"],[[129305,129407],"disallowed"],[[129408,129412],"valid",[],"NV8"],[[129413,129471],"disallowed"],[[129472,129472],"valid",[],"NV8"],[[129473,131069],"disallowed"],[[131070,131071],"disallowed"],[[131072,173782],"valid"],[[173783,173823],"disallowed"],[[173824,177972],"valid"],[[177973,177983],"disallowed"],[[177984,178205],"valid"],[[178206,178207],"disallowed"],[[178208,183969],"valid"],[[183970,194559],"disallowed"],[[194560,194560],"mapped",[20029]],[[194561,194561],"mapped",[20024]],[[194562,194562],"mapped",[20033]],[[194563,194563],"mapped",[131362]],[[194564,194564],"mapped",[20320]],[[194565,194565],"mapped",[20398]],[[194566,194566],"mapped",[20411]],[[194567,194567],"mapped",[20482]],[[194568,194568],"mapped",[20602]],[[194569,194569],"mapped",[20633]],[[194570,194570],"mapped",[20711]],[[194571,194571],"mapped",[20687]],[[194572,194572],"mapped",[13470]],[[194573,194573],"mapped",[132666]],[[194574,194574],"mapped",[20813]],[[194575,194575],"mapped",[20820]],[[194576,194576],"mapped",[20836]],[[194577,194577],"mapped",[20855]],[[194578,194578],"mapped",[132380]],[[194579,194579],"mapped",[13497]],[[194580,194580],"mapped",[20839]],[[194581,194581],"mapped",[20877]],[[194582,194582],"mapped",[132427]],[[194583,194583],"mapped",[20887]],[[194584,194584],"mapped",[20900]],[[194585,194585],"mapped",[20172]],[[194586,194586],"mapped",[20908]],[[194587,194587],"mapped",[20917]],[[194588,194588],"mapped",[168415]],[[194589,194589],"mapped",[20981]],[[194590,194590],"mapped",[20995]],[[194591,194591],"mapped",[13535]],[[194592,194592],"mapped",[21051]],[[194593,194593],"mapped",[21062]],[[194594,194594],"mapped",[21106]],[[194595,194595],"mapped",[21111]],[[194596,194596],"mapped",[13589]],[[194597,194597],"mapped",[21191]],[[194598,194598],"mapped",[21193]],[[194599,194599],"mapped",[21220]],[[194600,194600],"mapped",[21242]],[[194601,194601],"mapped",[21253]],[[194602,194602],"mapped",[21254]],[[194603,194603],"mapped",[21271]],[[194604,194604],"mapped",[21321]],[[194605,194605],"mapped",[21329]],[[194606,194606],"mapped",[21338]],[[194607,194607],"mapped",[21363]],[[194608,194608],"mapped",[21373]],[[194609,194611],"mapped",[21375]],[[194612,194612],"mapped",[133676]],[[194613,194613],"mapped",[28784]],[[194614,194614],"mapped",[21450]],[[194615,194615],"mapped",[21471]],[[194616,194616],"mapped",[133987]],[[194617,194617],"mapped",[21483]],[[194618,194618],"mapped",[21489]],[[194619,194619],"mapped",[21510]],[[194620,194620],"mapped",[21662]],[[194621,194621],"mapped",[21560]],[[194622,194622],"mapped",[21576]],[[194623,194623],"mapped",[21608]],[[194624,194624],"mapped",[21666]],[[194625,194625],"mapped",[21750]],[[194626,194626],"mapped",[21776]],[[194627,194627],"mapped",[21843]],[[194628,194628],"mapped",[21859]],[[194629,194630],"mapped",[21892]],[[194631,194631],"mapped",[21913]],[[194632,194632],"mapped",[21931]],[[194633,194633],"mapped",[21939]],[[194634,194634],"mapped",[21954]],[[194635,194635],"mapped",[22294]],[[194636,194636],"mapped",[22022]],[[194637,194637],"mapped",[22295]],[[194638,194638],"mapped",[22097]],[[194639,194639],"mapped",[22132]],[[194640,194640],"mapped",[20999]],[[194641,194641],"mapped",[22766]],[[194642,194642],"mapped",[22478]],[[194643,194643],"mapped",[22516]],[[194644,194644],"mapped",[22541]],[[194645,194645],"mapped",[22411]],[[194646,194646],"mapped",[22578]],[[194647,194647],"mapped",[22577]],[[194648,194648],"mapped",[22700]],[[194649,194649],"mapped",[136420]],[[194650,194650],"mapped",[22770]],[[194651,194651],"mapped",[22775]],[[194652,194652],"mapped",[22790]],[[194653,194653],"mapped",[22810]],[[194654,194654],"mapped",[22818]],[[194655,194655],"mapped",[22882]],[[194656,194656],"mapped",[136872]],[[194657,194657],"mapped",[136938]],[[194658,194658],"mapped",[23020]],[[194659,194659],"mapped",[23067]],[[194660,194660],"mapped",[23079]],[[194661,194661],"mapped",[23000]],[[194662,194662],"mapped",[23142]],[[194663,194663],"mapped",[14062]],[[194664,194664],"disallowed"],[[194665,194665],"mapped",[23304]],[[194666,194667],"mapped",[23358]],[[194668,194668],"mapped",[137672]],[[194669,194669],"mapped",[23491]],[[194670,194670],"mapped",[23512]],[[194671,194671],"mapped",[23527]],[[194672,194672],"mapped",[23539]],[[194673,194673],"mapped",[138008]],[[194674,194674],"mapped",[23551]],[[194675,194675],"mapped",[23558]],[[194676,194676],"disallowed"],[[194677,194677],"mapped",[23586]],[[194678,194678],"mapped",[14209]],[[194679,194679],"mapped",[23648]],[[194680,194680],"mapped",[23662]],[[194681,194681],"mapped",[23744]],[[194682,194682],"mapped",[23693]],[[194683,194683],"mapped",[138724]],[[194684,194684],"mapped",[23875]],[[194685,194685],"mapped",[138726]],[[194686,194686],"mapped",[23918]],[[194687,194687],"mapped",[23915]],[[194688,194688],"mapped",[23932]],[[194689,194689],"mapped",[24033]],[[194690,194690],"mapped",[24034]],[[194691,194691],"mapped",[14383]],[[194692,194692],"mapped",[24061]],[[194693,194693],"mapped",[24104]],[[194694,194694],"mapped",[24125]],[[194695,194695],"mapped",[24169]],[[194696,194696],"mapped",[14434]],[[194697,194697],"mapped",[139651]],[[194698,194698],"mapped",[14460]],[[194699,194699],"mapped",[24240]],[[194700,194700],"mapped",[24243]],[[194701,194701],"mapped",[24246]],[[194702,194702],"mapped",[24266]],[[194703,194703],"mapped",[172946]],[[194704,194704],"mapped",[24318]],[[194705,194706],"mapped",[140081]],[[194707,194707],"mapped",[33281]],[[194708,194709],"mapped",[24354]],[[194710,194710],"mapped",[14535]],[[194711,194711],"mapped",[144056]],[[194712,194712],"mapped",[156122]],[[194713,194713],"mapped",[24418]],[[194714,194714],"mapped",[24427]],[[194715,194715],"mapped",[14563]],[[194716,194716],"mapped",[24474]],[[194717,194717],"mapped",[24525]],[[194718,194718],"mapped",[24535]],[[194719,194719],"mapped",[24569]],[[194720,194720],"mapped",[24705]],[[194721,194721],"mapped",[14650]],[[194722,194722],"mapped",[14620]],[[194723,194723],"mapped",[24724]],[[194724,194724],"mapped",[141012]],[[194725,194725],"mapped",[24775]],[[194726,194726],"mapped",[24904]],[[194727,194727],"mapped",[24908]],[[194728,194728],"mapped",[24910]],[[194729,194729],"mapped",[24908]],[[194730,194730],"mapped",[24954]],[[194731,194731],"mapped",[24974]],[[194732,194732],"mapped",[25010]],[[194733,194733],"mapped",[24996]],[[194734,194734],"mapped",[25007]],[[194735,194735],"mapped",[25054]],[[194736,194736],"mapped",[25074]],[[194737,194737],"mapped",[25078]],[[194738,194738],"mapped",[25104]],[[194739,194739],"mapped",[25115]],[[194740,194740],"mapped",[25181]],[[194741,194741],"mapped",[25265]],[[194742,194742],"mapped",[25300]],[[194743,194743],"mapped",[25424]],[[194744,194744],"mapped",[142092]],[[194745,194745],"mapped",[25405]],[[194746,194746],"mapped",[25340]],[[194747,194747],"mapped",[25448]],[[194748,194748],"mapped",[25475]],[[194749,194749],"mapped",[25572]],[[194750,194750],"mapped",[142321]],[[194751,194751],"mapped",[25634]],[[194752,194752],"mapped",[25541]],[[194753,194753],"mapped",[25513]],[[194754,194754],"mapped",[14894]],[[194755,194755],"mapped",[25705]],[[194756,194756],"mapped",[25726]],[[194757,194757],"mapped",[25757]],[[194758,194758],"mapped",[25719]],[[194759,194759],"mapped",[14956]],[[194760,194760],"mapped",[25935]],[[194761,194761],"mapped",[25964]],[[194762,194762],"mapped",[143370]],[[194763,194763],"mapped",[26083]],[[194764,194764],"mapped",[26360]],[[194765,194765],"mapped",[26185]],[[194766,194766],"mapped",[15129]],[[194767,194767],"mapped",[26257]],[[194768,194768],"mapped",[15112]],[[194769,194769],"mapped",[15076]],[[194770,194770],"mapped",[20882]],[[194771,194771],"mapped",[20885]],[[194772,194772],"mapped",[26368]],[[194773,194773],"mapped",[26268]],[[194774,194774],"mapped",[32941]],[[194775,194775],"mapped",[17369]],[[194776,194776],"mapped",[26391]],[[194777,194777],"mapped",[26395]],[[194778,194778],"mapped",[26401]],[[194779,194779],"mapped",[26462]],[[194780,194780],"mapped",[26451]],[[194781,194781],"mapped",[144323]],[[194782,194782],"mapped",[15177]],[[194783,194783],"mapped",[26618]],[[194784,194784],"mapped",[26501]],[[194785,194785],"mapped",[26706]],[[194786,194786],"mapped",[26757]],[[194787,194787],"mapped",[144493]],[[194788,194788],"mapped",[26766]],[[194789,194789],"mapped",[26655]],[[194790,194790],"mapped",[26900]],[[194791,194791],"mapped",[15261]],[[194792,194792],"mapped",[26946]],[[194793,194793],"mapped",[27043]],[[194794,194794],"mapped",[27114]],[[194795,194795],"mapped",[27304]],[[194796,194796],"mapped",[145059]],[[194797,194797],"mapped",[27355]],[[194798,194798],"mapped",[15384]],[[194799,194799],"mapped",[27425]],[[194800,194800],"mapped",[145575]],[[194801,194801],"mapped",[27476]],[[194802,194802],"mapped",[15438]],[[194803,194803],"mapped",[27506]],[[194804,194804],"mapped",[27551]],[[194805,194805],"mapped",[27578]],[[194806,194806],"mapped",[27579]],[[194807,194807],"mapped",[146061]],[[194808,194808],"mapped",[138507]],[[194809,194809],"mapped",[146170]],[[194810,194810],"mapped",[27726]],[[194811,194811],"mapped",[146620]],[[194812,194812],"mapped",[27839]],[[194813,194813],"mapped",[27853]],[[194814,194814],"mapped",[27751]],[[194815,194815],"mapped",[27926]],[[194816,194816],"mapped",[27966]],[[194817,194817],"mapped",[28023]],[[194818,194818],"mapped",[27969]],[[194819,194819],"mapped",[28009]],[[194820,194820],"mapped",[28024]],[[194821,194821],"mapped",[28037]],[[194822,194822],"mapped",[146718]],[[194823,194823],"mapped",[27956]],[[194824,194824],"mapped",[28207]],[[194825,194825],"mapped",[28270]],[[194826,194826],"mapped",[15667]],[[194827,194827],"mapped",[28363]],[[194828,194828],"mapped",[28359]],[[194829,194829],"mapped",[147153]],[[194830,194830],"mapped",[28153]],[[194831,194831],"mapped",[28526]],[[194832,194832],"mapped",[147294]],[[194833,194833],"mapped",[147342]],[[194834,194834],"mapped",[28614]],[[194835,194835],"mapped",[28729]],[[194836,194836],"mapped",[28702]],[[194837,194837],"mapped",[28699]],[[194838,194838],"mapped",[15766]],[[194839,194839],"mapped",[28746]],[[194840,194840],"mapped",[28797]],[[194841,194841],"mapped",[28791]],[[194842,194842],"mapped",[28845]],[[194843,194843],"mapped",[132389]],[[194844,194844],"mapped",[28997]],[[194845,194845],"mapped",[148067]],[[194846,194846],"mapped",[29084]],[[194847,194847],"disallowed"],[[194848,194848],"mapped",[29224]],[[194849,194849],"mapped",[29237]],[[194850,194850],"mapped",[29264]],[[194851,194851],"mapped",[149000]],[[194852,194852],"mapped",[29312]],[[194853,194853],"mapped",[29333]],[[194854,194854],"mapped",[149301]],[[194855,194855],"mapped",[149524]],[[194856,194856],"mapped",[29562]],[[194857,194857],"mapped",[29579]],[[194858,194858],"mapped",[16044]],[[194859,194859],"mapped",[29605]],[[194860,194861],"mapped",[16056]],[[194862,194862],"mapped",[29767]],[[194863,194863],"mapped",[29788]],[[194864,194864],"mapped",[29809]],[[194865,194865],"mapped",[29829]],[[194866,194866],"mapped",[29898]],[[194867,194867],"mapped",[16155]],[[194868,194868],"mapped",[29988]],[[194869,194869],"mapped",[150582]],[[194870,194870],"mapped",[30014]],[[194871,194871],"mapped",[150674]],[[194872,194872],"mapped",[30064]],[[194873,194873],"mapped",[139679]],[[194874,194874],"mapped",[30224]],[[194875,194875],"mapped",[151457]],[[194876,194876],"mapped",[151480]],[[194877,194877],"mapped",[151620]],[[194878,194878],"mapped",[16380]],[[194879,194879],"mapped",[16392]],[[194880,194880],"mapped",[30452]],[[194881,194881],"mapped",[151795]],[[194882,194882],"mapped",[151794]],[[194883,194883],"mapped",[151833]],[[194884,194884],"mapped",[151859]],[[194885,194885],"mapped",[30494]],[[194886,194887],"mapped",[30495]],[[194888,194888],"mapped",[30538]],[[194889,194889],"mapped",[16441]],[[194890,194890],"mapped",[30603]],[[194891,194891],"mapped",[16454]],[[194892,194892],"mapped",[16534]],[[194893,194893],"mapped",[152605]],[[194894,194894],"mapped",[30798]],[[194895,194895],"mapped",[30860]],[[194896,194896],"mapped",[30924]],[[194897,194897],"mapped",[16611]],[[194898,194898],"mapped",[153126]],[[194899,194899],"mapped",[31062]],[[194900,194900],"mapped",[153242]],[[194901,194901],"mapped",[153285]],[[194902,194902],"mapped",[31119]],[[194903,194903],"mapped",[31211]],[[194904,194904],"mapped",[16687]],[[194905,194905],"mapped",[31296]],[[194906,194906],"mapped",[31306]],[[194907,194907],"mapped",[31311]],[[194908,194908],"mapped",[153980]],[[194909,194910],"mapped",[154279]],[[194911,194911],"disallowed"],[[194912,194912],"mapped",[16898]],[[194913,194913],"mapped",[154539]],[[194914,194914],"mapped",[31686]],[[194915,194915],"mapped",[31689]],[[194916,194916],"mapped",[16935]],[[194917,194917],"mapped",[154752]],[[194918,194918],"mapped",[31954]],[[194919,194919],"mapped",[17056]],[[194920,194920],"mapped",[31976]],[[194921,194921],"mapped",[31971]],[[194922,194922],"mapped",[32000]],[[194923,194923],"mapped",[155526]],[[194924,194924],"mapped",[32099]],[[194925,194925],"mapped",[17153]],[[194926,194926],"mapped",[32199]],[[194927,194927],"mapped",[32258]],[[194928,194928],"mapped",[32325]],[[194929,194929],"mapped",[17204]],[[194930,194930],"mapped",[156200]],[[194931,194931],"mapped",[156231]],[[194932,194932],"mapped",[17241]],[[194933,194933],"mapped",[156377]],[[194934,194934],"mapped",[32634]],[[194935,194935],"mapped",[156478]],[[194936,194936],"mapped",[32661]],[[194937,194937],"mapped",[32762]],[[194938,194938],"mapped",[32773]],[[194939,194939],"mapped",[156890]],[[194940,194940],"mapped",[156963]],[[194941,194941],"mapped",[32864]],[[194942,194942],"mapped",[157096]],[[194943,194943],"mapped",[32880]],[[194944,194944],"mapped",[144223]],[[194945,194945],"mapped",[17365]],[[194946,194946],"mapped",[32946]],[[194947,194947],"mapped",[33027]],[[194948,194948],"mapped",[17419]],[[194949,194949],"mapped",[33086]],[[194950,194950],"mapped",[23221]],[[194951,194951],"mapped",[157607]],[[194952,194952],"mapped",[157621]],[[194953,194953],"mapped",[144275]],[[194954,194954],"mapped",[144284]],[[194955,194955],"mapped",[33281]],[[194956,194956],"mapped",[33284]],[[194957,194957],"mapped",[36766]],[[194958,194958],"mapped",[17515]],[[194959,194959],"mapped",[33425]],[[194960,194960],"mapped",[33419]],[[194961,194961],"mapped",[33437]],[[194962,194962],"mapped",[21171]],[[194963,194963],"mapped",[33457]],[[194964,194964],"mapped",[33459]],[[194965,194965],"mapped",[33469]],[[194966,194966],"mapped",[33510]],[[194967,194967],"mapped",[158524]],[[194968,194968],"mapped",[33509]],[[194969,194969],"mapped",[33565]],[[194970,194970],"mapped",[33635]],[[194971,194971],"mapped",[33709]],[[194972,194972],"mapped",[33571]],[[194973,194973],"mapped",[33725]],[[194974,194974],"mapped",[33767]],[[194975,194975],"mapped",[33879]],[[194976,194976],"mapped",[33619]],[[194977,194977],"mapped",[33738]],[[194978,194978],"mapped",[33740]],[[194979,194979],"mapped",[33756]],[[194980,194980],"mapped",[158774]],[[194981,194981],"mapped",[159083]],[[194982,194982],"mapped",[158933]],[[194983,194983],"mapped",[17707]],[[194984,194984],"mapped",[34033]],[[194985,194985],"mapped",[34035]],[[194986,194986],"mapped",[34070]],[[194987,194987],"mapped",[160714]],[[194988,194988],"mapped",[34148]],[[194989,194989],"mapped",[159532]],[[194990,194990],"mapped",[17757]],[[194991,194991],"mapped",[17761]],[[194992,194992],"mapped",[159665]],[[194993,194993],"mapped",[159954]],[[194994,194994],"mapped",[17771]],[[194995,194995],"mapped",[34384]],[[194996,194996],"mapped",[34396]],[[194997,194997],"mapped",[34407]],[[194998,194998],"mapped",[34409]],[[194999,194999],"mapped",[34473]],[[195000,195000],"mapped",[34440]],[[195001,195001],"mapped",[34574]],[[195002,195002],"mapped",[34530]],[[195003,195003],"mapped",[34681]],[[195004,195004],"mapped",[34600]],[[195005,195005],"mapped",[34667]],[[195006,195006],"mapped",[34694]],[[195007,195007],"disallowed"],[[195008,195008],"mapped",[34785]],[[195009,195009],"mapped",[34817]],[[195010,195010],"mapped",[17913]],[[195011,195011],"mapped",[34912]],[[195012,195012],"mapped",[34915]],[[195013,195013],"mapped",[161383]],[[195014,195014],"mapped",[35031]],[[195015,195015],"mapped",[35038]],[[195016,195016],"mapped",[17973]],[[195017,195017],"mapped",[35066]],[[195018,195018],"mapped",[13499]],[[195019,195019],"mapped",[161966]],[[195020,195020],"mapped",[162150]],[[195021,195021],"mapped",[18110]],[[195022,195022],"mapped",[18119]],[[195023,195023],"mapped",[35488]],[[195024,195024],"mapped",[35565]],[[195025,195025],"mapped",[35722]],[[195026,195026],"mapped",[35925]],[[195027,195027],"mapped",[162984]],[[195028,195028],"mapped",[36011]],[[195029,195029],"mapped",[36033]],[[195030,195030],"mapped",[36123]],[[195031,195031],"mapped",[36215]],[[195032,195032],"mapped",[163631]],[[195033,195033],"mapped",[133124]],[[195034,195034],"mapped",[36299]],[[195035,195035],"mapped",[36284]],[[195036,195036],"mapped",[36336]],[[195037,195037],"mapped",[133342]],[[195038,195038],"mapped",[36564]],[[195039,195039],"mapped",[36664]],[[195040,195040],"mapped",[165330]],[[195041,195041],"mapped",[165357]],[[195042,195042],"mapped",[37012]],[[195043,195043],"mapped",[37105]],[[195044,195044],"mapped",[37137]],[[195045,195045],"mapped",[165678]],[[195046,195046],"mapped",[37147]],[[195047,195047],"mapped",[37432]],[[195048,195048],"mapped",[37591]],[[195049,195049],"mapped",[37592]],[[195050,195050],"mapped",[37500]],[[195051,195051],"mapped",[37881]],[[195052,195052],"mapped",[37909]],[[195053,195053],"mapped",[166906]],[[195054,195054],"mapped",[38283]],[[195055,195055],"mapped",[18837]],[[195056,195056],"mapped",[38327]],[[195057,195057],"mapped",[167287]],[[195058,195058],"mapped",[18918]],[[195059,195059],"mapped",[38595]],[[195060,195060],"mapped",[23986]],[[195061,195061],"mapped",[38691]],[[195062,195062],"mapped",[168261]],[[195063,195063],"mapped",[168474]],[[195064,195064],"mapped",[19054]],[[195065,195065],"mapped",[19062]],[[195066,195066],"mapped",[38880]],[[195067,195067],"mapped",[168970]],[[195068,195068],"mapped",[19122]],[[195069,195069],"mapped",[169110]],[[195070,195071],"mapped",[38923]],[[195072,195072],"mapped",[38953]],[[195073,195073],"mapped",[169398]],[[195074,195074],"mapped",[39138]],[[195075,195075],"mapped",[19251]],[[195076,195076],"mapped",[39209]],[[195077,195077],"mapped",[39335]],[[195078,195078],"mapped",[39362]],[[195079,195079],"mapped",[39422]],[[195080,195080],"mapped",[19406]],[[195081,195081],"mapped",[170800]],[[195082,195082],"mapped",[39698]],[[195083,195083],"mapped",[40000]],[[195084,195084],"mapped",[40189]],[[195085,195085],"mapped",[19662]],[[195086,195086],"mapped",[19693]],[[195087,195087],"mapped",[40295]],[[195088,195088],"mapped",[172238]],[[195089,195089],"mapped",[19704]],[[195090,195090],"mapped",[172293]],[[195091,195091],"mapped",[172558]],[[195092,195092],"mapped",[172689]],[[195093,195093],"mapped",[40635]],[[195094,195094],"mapped",[19798]],[[195095,195095],"mapped",[40697]],[[195096,195096],"mapped",[40702]],[[195097,195097],"mapped",[40709]],[[195098,195098],"mapped",[40719]],[[195099,195099],"mapped",[40726]],[[195100,195100],"mapped",[40763]],[[195101,195101],"mapped",[173568]],[[195102,196605],"disallowed"],[[196606,196607],"disallowed"],[[196608,262141],"disallowed"],[[262142,262143],"disallowed"],[[262144,327677],"disallowed"],[[327678,327679],"disallowed"],[[327680,393213],"disallowed"],[[393214,393215],"disallowed"],[[393216,458749],"disallowed"],[[458750,458751],"disallowed"],[[458752,524285],"disallowed"],[[524286,524287],"disallowed"],[[524288,589821],"disallowed"],[[589822,589823],"disallowed"],[[589824,655357],"disallowed"],[[655358,655359],"disallowed"],[[655360,720893],"disallowed"],[[720894,720895],"disallowed"],[[720896,786429],"disallowed"],[[786430,786431],"disallowed"],[[786432,851965],"disallowed"],[[851966,851967],"disallowed"],[[851968,917501],"disallowed"],[[917502,917503],"disallowed"],[[917504,917504],"disallowed"],[[917505,917505],"disallowed"],[[917506,917535],"disallowed"],[[917536,917631],"disallowed"],[[917632,917759],"disallowed"],[[917760,917999],"ignored"],[[918000,983037],"disallowed"],[[983038,983039],"disallowed"],[[983040,1048573],"disallowed"],[[1048574,1048575],"disallowed"],[[1048576,1114109],"disallowed"],[[1114110,1114111],"disallowed"]]')}},A={};function t(r){var s=A[r];if(void 0!==s)return s.exports;var a=A[r]={id:r,loaded:!1,exports:{}};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}t.n=e=>{var A=e&&e.__esModule?()=>e.default:()=>e;return t.d(A,{a:A}),A},t.d=(e,A)=>{for(var r in A)t.o(A,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:A[r]})},t.o=(e,A)=>Object.prototype.hasOwnProperty.call(e,A),t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),t(3607)})();
\ No newline at end of file
diff --git a/.github/actions/report-flaky-tests/dist/index.js.LICENSE.txt b/.github/actions/report-flaky-tests/dist/index.js.LICENSE.txt
new file mode 100644
index 00000000000..2d2e830f54c
--- /dev/null
+++ b/.github/actions/report-flaky-tests/dist/index.js.LICENSE.txt
@@ -0,0 +1,41 @@
+/*!
+ * fill-range
+ *
+ * Copyright (c) 2014-present, Jon Schlinkert.
+ * Licensed under the MIT License.
+ */
+
+/*!
+ * is-number
+ *
+ * Copyright (c) 2014-present, Jon Schlinkert.
+ * Released under the MIT License.
+ */
+
+/*!
+ * is-plain-object
+ *
+ * Copyright (c) 2014-2017, Jon Schlinkert.
+ * Released under the MIT License.
+ */
+
+/*!
+ * to-regex-range
+ *
+ * Copyright (c) 2015-present, Jon Schlinkert.
+ * Released under the MIT License.
+ */
+
+/*! formdata-polyfill. MIT License. Jimmy Wärting */
+
+/*! ws. MIT License. Einar Otto Stangvik */
+
+/**
+ * @license React
+ * react-is.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
diff --git a/.github/actions/setup-woocommerce-monorepo/action.yml b/.github/actions/setup-woocommerce-monorepo/action.yml
index 7f870505929..165b6930322 100644
--- a/.github/actions/setup-woocommerce-monorepo/action.yml
+++ b/.github/actions/setup-woocommerce-monorepo/action.yml
@@ -16,6 +16,9 @@ inputs:
pull-playwright-cache:
description: 'Given a boolean value, invokes Playwright dependencies caching.'
default: false
+ pull-package-deps:
+ description: 'Given a string value, will pull the package specific dependencies cache.'
+ default: false
runs:
using: 'composite'
steps:
@@ -31,27 +34,46 @@ runs:
uses: 'actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65'
with:
node-version-file: '.nvmrc'
- # We only want to use the cache if something is being installed.
- cache: ${{ inputs.install != 'false' && 'pnpm' || '' }}
+ # The built-in caching is not fit to per-package caching we are aiming.
+ cache: ''
- name: 'Setup PHP'
if: ${{ inputs.php-version != 'false' }}
uses: 'shivammathur/setup-php@a36e1e52ff4a1c9e9c9be31551ee4712a6cb6bd0'
with:
php-version: '${{ inputs.php-version }}'
coverage: 'none'
+ - name: 'Cache: identify pnpm caching directory'
+ if: ${{ inputs.pull-package-deps != 'false' }}
+ shell: 'bash'
+ run: |
+ echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
+ - name: 'Cache: pnpm downloads'
+ if: ${{ inputs.pull-package-deps != 'false' }}
+ uses: 'actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319'
+ with:
+ path: "${{ env.PNPM_STORE_PATH }}"
+ key: "${{ runner.os }}-pnpm-${{ inputs.pull-package-deps }}-build:${{ inputs.build-type }}-${{ hashFiles( 'pnpm-lock.yaml' ) }}"
+ restore-keys: '${{ runner.os }}-pnpm-${{ inputs.pull-package-deps }}-build:${{ inputs.build-type }}-'
+ - name: 'Cache: node cache'
+ if: ${{ inputs.pull-package-deps != 'false' }}
+ uses: 'actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319'
+ with:
+ path: './node_modules/.cache'
+ key: "${{ runner.os }}-node-cache-${{ inputs.pull-package-deps }}-${{ hashFiles( 'pnpm-lock.yaml' ) }}"
+ restore-keys: '${{ runner.os }}-node-cache-${{ inputs.pull-package-deps }}-'
- name: 'Cache Composer Dependencies'
- if: ${{ inputs.build == 'false' }}
+ if: ${{ inputs.pull-package-deps != 'false' }}
uses: 'actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319'
with:
path: '~/.cache/composer/files'
- key: "${{ runner.os }}-composer-${{ hashFiles( '**/composer.lock' ) }}"
- restore-keys: '${{ runner.os }}-composer-'
+ key: "${{ runner.os }}-composer-${{ inputs.pull-package-deps }}-${{ hashFiles( 'packages/*/*/composer.lock', 'plugins/*/composer.lock' ) }}"
+ restore-keys: '${{ runner.os }}-composer-${{ inputs.pull-package-deps }}-'
- name: 'Cache: playwright downloads'
if: ${{ inputs.pull-playwright-cache != 'false' }}
uses: 'actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319'
with:
path: '~/.cache/ms-playwright/'
- key: "${{ runner.os }}-playwright-${{ hashFiles( '**/pnpm-lock.yaml' ) }}"
+ key: "${{ runner.os }}-playwright-${{ hashFiles( 'pnpm-lock.yaml' ) }}"
restore-keys: '${{ runner.os }}-playwright-'
- name: 'Parse Project Filters'
id: 'project-filters'
@@ -63,9 +85,16 @@ runs:
# Boolean inputs aren't parsed into filters so it'll either be "true" or there will be a filter.
if: ${{ inputs.install == 'true' || steps.project-filters.outputs.install != '' }}
shell: 'bash'
- run: 'pnpm install'
- # `pnpm install` filtering is broken: https://github.com/pnpm/pnpm/issues/6300
- # run: 'pnpm install ${{ steps.project-filters.outputs.install }}'
+ # The installation command is a bit odd as it's a workaround for know bug - https://github.com/pnpm/pnpm/issues/6300.
+ run: |
+ if [[ '${{ inputs.install }}' == '@woocommerce/plugin-woocommerce...' && '${{ inputs.build-type }}' == 'backend' ]]; then
+ # PHPUnit/REST testing optimized installation of the deps: minimalistic and parallellized between PHP/JS.
+ # JS deps installation is abit hard-core, but all we need actually is wp-env and playwright - we are good at that regard.
+ composer install --working-dir=./plugins/woocommerce --quiet &
+ pnpm install --filter='@woocommerce/plugin-woocommerce' --frozen-lockfile --config.dedupe-peer-dependents=false --ignore-scripts
+ else
+ pnpm install ${{ steps.project-filters.outputs.install }} --frozen-lockfile ${{ steps.project-filters.outputs.install != '' && '--config.dedupe-peer-dependents=false' || '' }}
+ fi
# We want to include an option to build projects using this action so that we can make
# sure that the build cache is always used when building projects.
- name: 'Cache Build Output'
@@ -75,6 +104,8 @@ runs:
- name: 'Build'
# Boolean inputs aren't parsed into filters so it'll either be "true" or there will be a filter.
if: ${{ inputs.build == 'true' || steps.project-filters.outputs.build != '' }}
+ env:
+ BROWSERSLIST_IGNORE_OLD_DATA: true
shell: 'bash'
run: |
if [[ '${{ inputs.build-type }}' == 'backend' ]]; then
diff --git a/.github/workflows/automate-team-review-assignment-config.yml b/.github/workflows/automate-team-review-assignment-config.yml
index 9415e015318..44dfe9a6a01 100644
--- a/.github/workflows/automate-team-review-assignment-config.yml
+++ b/.github/workflows/automate-team-review-assignment-config.yml
@@ -12,4 +12,4 @@ jobs:
uses: acq688/Request-Reviewer-For-Team-Action@v1.1
with:
config: '.github/automate-team-review-assignment-config.yml'
- GITHUB_TOKEN: ${{ secrets.FINE_GRAINED_TOKEN_ACTIONS }}
+ GITHUB_TOKEN: ${{ secrets.PR_ASSIGN_TOKEN }}
diff --git a/.github/workflows/build-live-branch.yml b/.github/workflows/build-live-branch.yml
index db1004e9530..aa3779cb719 100644
--- a/.github/workflows/build-live-branch.yml
+++ b/.github/workflows/build-live-branch.yml
@@ -30,12 +30,8 @@ jobs:
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
-
- - name: Cache PNPM Dependencies
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65
with:
- node-version-file: .nvmrc
- cache: pnpm
+ pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Prepare plugin zips
id: prepare
diff --git a/.github/workflows/build-release-zip-file.yml b/.github/workflows/build-release-zip-file.yml
index 9499cf2e617..94ae898d2a6 100644
--- a/.github/workflows/build-release-zip-file.yml
+++ b/.github/workflows/build-release-zip-file.yml
@@ -20,6 +20,8 @@ jobs:
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
+ with:
+ pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Build zip
working-directory: plugins/woocommerce
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5c4ba6383d3..a7e4bfad4f8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,48 +14,15 @@ on:
required: true
default: 'default'
type: string
- workflow_dispatch:
- inputs:
- pr_simulate:
- description: 'Would you like to run CI on a pull request? If so, enter the PR number here. If blank, the entire suite will be run.'
- type: string
- default: ''
concurrency:
- group: '${{ github.workflow }}-${{ github.ref }}'
+ group: '${{ github.workflow }}-${{ github.ref }}-${{ inputs.trigger }}'
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
- dispatch-handler:
- name: 'Handle dispatched workflow'
- runs-on: 'ubuntu-20.04'
- if: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_simulate }}
- outputs:
- head: ${{ steps.pr-info.outputs.head }}
- base: ${{ steps.pr-info.outputs.base }}
-
- steps:
- - uses: actions/github-script@v7
- name: 'Grab PR info.'
- id: 'pr-info'
- env:
- PR: ${{ inputs.pr_simulate }}
- with:
- retries: 3
- script: |
- if ( ! process.env.PR ) {
- return;
- }
- const PR = await github.rest.pulls.get( {
- pull_number: process.env.PR,
- repo: context.repo.repo,
- owner: context.repo.owner,
- } );
- core.setOutput( 'head', PR.data.head.ref );
- core.setOutput( 'base', PR.data.base.ref );
project-jobs:
# Since this is a monorepo, not every pull request or change is going to impact every project.
# Instead of running CI tasks on all projects indiscriminately, we use a command to detect
@@ -63,12 +30,6 @@ jobs:
# matrices that we can use to run CI tasks only on the projects that need them.
name: 'Build Project Jobs'
runs-on: 'ubuntu-20.04'
- needs: 'dispatch-handler'
- # Because forks of this repository may want to skip running this CI automatically, but still
- # be able to run it via workflow_dispatch, if the SKIP_CI variable is truthy, and we're not
- # running from a workflow_dispatch, we'll skip generating the project matrix and any jobs.
- # Because dispatch-handler may be skipped, we need the always() here.
- if: ${{ always() && ( github.event_name == 'workflow_dispatch' || ! vars.SKIP_CI ) }}
outputs:
lint-jobs: ${{ steps.project-jobs.outputs.lint-jobs }}
test-jobs: ${{ steps.project-jobs.outputs.test-jobs }}
@@ -78,24 +39,19 @@ jobs:
name: 'Checkout'
with:
fetch-depth: 0
- # If the workflow wasn't triggered by dispatch, this will be empty and use defaults.
- ref: ${{ needs.dispatch-handler.outputs.head }}
-
- uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Setup Monorepo'
with:
php-version: false # We don't want to waste time installing PHP since we aren't using it in this job.
-
- uses: actions/github-script@v7
name: 'Build Matrix'
id: 'project-jobs'
- env:
- PR_SIM: ${{ needs.dispatch-handler.outputs.base }}
with:
script: |
- const prSim = process.env.PR_SIM;
-
- let baseRef = prSim || ${{ toJson( github.base_ref ) }};
+ // Intended behaviour of the jobs generation:
+ // - PRs: run CI jobs aiming PRs and filter out jobs based on the content changes
+ // - Pushes: run CI jobs aiming pushes without filtering based on the content changes
+ let baseRef = ${{ toJson( github.base_ref ) }};
if ( baseRef ) {
baseRef = `--base-ref origin/${ baseRef }`;
}
@@ -118,65 +74,48 @@ jobs:
githubEvent = trigger;
}
- // Override the event 'workflow_dispatch' event type if we're simulating a PR.
- if ( prSim ) {
- githubEvent = 'pull_request';
- }
-
const child_process = require( 'node:child_process' );
child_process.execSync( `pnpm utils ci-jobs ${ baseRef } --event ${ githubEvent }` );
-
+
project-lint-jobs:
name: "Lint - ${{ matrix.projectName }} ${{ matrix.optional && ' (optional)' || ''}}"
runs-on: 'ubuntu-20.04'
- needs: [
- 'project-jobs',
- 'dispatch-handler'
- ]
- # Because dispatch-handler may be skipped, we need the always() here.
- if: ${{ always() && needs.project-jobs.outputs.lint-jobs != '[]' && ( github.event_name == 'pull_request' || inputs.pr_simulate != '' ) }}
+ needs: 'project-jobs'
+ if: ${{ needs.project-jobs.outputs.lint-jobs != '[]' && github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON( needs.project-jobs.outputs.lint-jobs ) }}
-
steps:
- uses: 'actions/checkout@v4'
name: 'Checkout'
with:
- fetch-depth: 0
- # If the workflow wasn't triggered by dispatch, this will be empty and use defaults.
- ref: ${{ needs.dispatch-handler.outputs.head }}
+ # the WooCommerce plugin package uses phpcs-changed for linting, which requires non-shallow git-history.
+ fetch-depth: ${{ ( ( matrix.projectName == '@woocommerce/plugin-woocommerce' && '0' ) || '1' ) }}
- uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Setup Monorepo'
id: 'setup-monorepo'
with:
install: '${{ matrix.projectName }}...'
+ pull-package-deps: '${{ matrix.projectName }}'
- name: 'Lint'
run: 'pnpm --filter="${{ matrix.projectName }}" ${{ matrix.command }}'
-
+
project-test-jobs:
name: "${{ matrix.name }}"
runs-on: 'ubuntu-20.04'
- needs: [
- 'project-jobs',
- 'dispatch-handler'
- ]
- if: ${{ always() && needs.project-jobs.outputs.test-jobs != '[]' }}
+ needs: 'project-jobs'
+ if: ${{ needs.project-jobs.outputs.test-jobs != '[]' }}
env: ${{ matrix.testEnv.envVars }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON( needs.project-jobs.outputs.test-jobs ) }}
-
steps:
- uses: 'actions/checkout@v4'
name: 'Checkout'
- with:
- # If the workflow wasn't triggered by dispatch, this will be empty and use defaults.
- ref: ${{ needs.dispatch-handler.outputs.head }}
- uses: './.github/actions/setup-woocommerce-monorepo'
name: 'Install Monorepo'
@@ -186,14 +125,16 @@ jobs:
build: ${{ ( github.ref_type == 'tag' && 'false' ) || matrix.projectName }}
build-type: ${{ ( matrix.testType == 'unit:php' && 'backend' ) || 'full' }}
pull-playwright-cache: ${{ matrix.testEnv.shouldCreate && matrix.testType == 'e2e' }}
+ pull-package-deps: '${{ matrix.projectName }}'
- name: 'Update wp-env config'
if: ${{ github.ref_type == 'tag' }}
env:
RELEASE_TAG: ${{ github.ref_name }}
ARTIFACT_NAME: ${{ github.ref_name == 'nightly' && 'woocommerce-trunk-nightly.zip' || 'woocommerce.zip' }}
- working-directory: ${{ matrix.projectPath }}
- run: node ./tests/e2e-pw/bin/override-wp-env-plugins.js
+ # band-aid to get the path to wp-env.json for blocks e2e tests, until they're migrated to plugins/woocommerce
+ WP_ENV_CONFIG_PATH: ${{ github.workspace }}/${{ matrix.testEnv.start == 'env:start:blocks' && 'plugins/woocommerce-blocks' || matrix.projectPath }}
+ run: node .github/workflows/scripts/override-wp-env-plugins.js
- name: 'Start Test Environment'
id: 'prepare-test-environment'
@@ -270,7 +211,7 @@ jobs:
'project-lint-jobs',
'project-test-jobs',
]
- if: ${{ always() && github.event_name == 'pull_request' }}
+ if: ${{ !cancelled() && github.event_name == 'pull_request' }}
steps:
- uses: 'actions/checkout@v4'
name: 'Checkout'
@@ -299,7 +240,7 @@ jobs:
'project-lint-jobs',
'project-test-jobs',
]
- if: ${{ always() && github.event_name != 'pull_request' && ! github.event.pull_request.head.repo.fork }}
+ if: ${{ !cancelled() && github.event_name != 'pull_request' && github.repository == 'woocommerce/woocommerce' }}
steps:
- uses: 'actions/checkout@v4'
name: 'Checkout'
@@ -336,7 +277,7 @@ jobs:
'project-jobs',
'project-test-jobs',
]
- if: ${{ always() && needs.project-jobs.outputs.report-jobs != '[]' && ! github.event.pull_request.head.repo.fork }}
+ if: ${{ !cancelled() && needs.project-jobs.outputs.report-jobs != '[]' && github.repository == 'woocommerce/woocommerce' }}
strategy:
fail-fast: false
matrix:
@@ -390,48 +331,42 @@ jobs:
-f suite="$REPORT_NAME" \
-f report_title="$REPORT_TITLE" \
--repo woocommerce/woocommerce-test-reports
-
+
report-flaky-tests:
name: 'Create issues for flaky tests'
- if: ${{ !cancelled() && ! github.event.pull_request.head.repo.fork }}
- needs: [ 'project-test-jobs' ]
+ if: ${{ !cancelled() && github.repository == 'woocommerce/woocommerce' && needs.project-jobs.outputs.test-jobs != '[]' }}
+ needs:
+ [
+ 'project-jobs',
+ 'project-test-jobs',
+ ]
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- - uses: actions/checkout@v4
- with:
- repository: WordPress/gutenberg
- ref: dbf201449e9736f672b61e422787d47659db327a
+ - uses: 'actions/checkout@v4'
+ name: 'Checkout'
- - uses: actions/download-artifact@v4
- id: download-artifact
+ - uses: 'actions/download-artifact@v4'
+ name: 'Download artifacts'
with:
pattern: flaky-tests*
path: flaky-tests
merge-multiple: true
- - name: 'Check if there are flaky tests reports'
+ - name: 'Merge flaky tests reports'
run: |
- downloadPath='${{ steps.download-artifact.outputs.download-path }}'
+ downloadPath='${{ steps.download-artifact.outputs.download-path || './flaky-tests' }}'
# make dir so that next step doesn't fail if it doesn't exist
mkdir -p $downloadPath
# any output means there are reports
echo "FLAKY_REPORTS=$(ls -A $downloadPath | head -1)" >> $GITHUB_ENV
- - name: 'Setup'
- if: ${{ !!env.FLAKY_REPORTS }}
- uses: ./.github/setup-node
-
- - name: 'Build packages'
- if: ${{ !!env.FLAKY_REPORTS }}
- run: npm run build:packages
-
- name: 'Report flaky tests'
if: ${{ !!env.FLAKY_REPORTS }}
- uses: ./packages/report-flaky-tests
+ uses: './.github/actions/report-flaky-tests'
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
label: 'metric: flaky e2e test'
diff --git a/.github/workflows/deprecated/blocks-playwright.yml b/.github/workflows/deprecated/blocks-playwright.yml
index 19b1d3e2b4b..d4c5ce3c98c 100644
--- a/.github/workflows/deprecated/blocks-playwright.yml
+++ b/.github/workflows/deprecated/blocks-playwright.yml
@@ -52,6 +52,7 @@ jobs:
install: '@woocommerce/plugin-woocommerce...'
build: '@woocommerce/plugin-woocommerce'
pull-playwright-cache: true
+ pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Install Playwright dependencies
run: pnpm exec playwright install chromium --with-deps
diff --git a/.github/workflows/mirrors.yml b/.github/workflows/mirrors.yml
index e0eb15cc71f..7cd52722d2f 100644
--- a/.github/workflows/mirrors.yml
+++ b/.github/workflows/mirrors.yml
@@ -18,6 +18,8 @@ jobs:
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
+ with:
+ pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Build zip
working-directory: plugins/woocommerce
diff --git a/.github/workflows/nightly-builds.yml b/.github/workflows/nightly-builds.yml
index 5f5427257a0..6a3e22fa123 100644
--- a/.github/workflows/nightly-builds.yml
+++ b/.github/workflows/nightly-builds.yml
@@ -24,6 +24,8 @@ jobs:
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
+ with:
+ pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Build zip
working-directory: plugins/woocommerce
diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml
index 1f3213e44cc..c0924e2aee7 100644
--- a/.github/workflows/package-release.yml
+++ b/.github/workflows/package-release.yml
@@ -27,6 +27,7 @@ jobs:
with:
install: true
build: './tools/package-release'
+ pull-package-deps: 'tools/package-release'
- name: Clean working directory
run: git checkout pnpm-lock.yaml # in case for whatever reason the lockfile is out of sync, there won't be interference with npm publish.
diff --git a/.github/workflows/pr-assess-bundle-size.yml b/.github/workflows/pr-assess-bundle-size.yml
index 30981368d5d..f552f6198ac 100644
--- a/.github/workflows/pr-assess-bundle-size.yml
+++ b/.github/workflows/pr-assess-bundle-size.yml
@@ -44,10 +44,16 @@ jobs:
# Both install and build are handled by compressed-size-action.
install: false
build: false
+ pull-package-deps: '@woocommerce/plugin-woocommerce'
+
- uses: preactjs/compressed-size-action@f780fd104362cfce9e118f9198df2ee37d12946c
+ env:
+ BROWSERSLIST_IGNORE_OLD_DATA: true
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
pattern: './{packages/js/!(*e2e*|*internal*|*test*|*plugin*|*create*),plugins/woocommerce-blocks}/{build,build-style}/**/*.{js,css}'
- clean-script: '--if-present distclean'
+ install-script: 'pnpm install --filter="@woocommerce/plugin-woocommerce..." --frozen-lockfile --config.dedupe-peer-dependents=false'
+ build-script: '--filter="@woocommerce/plugin-woocommerce" build'
+ clean-script: '--if-present buildclean'
minimum-change-threshold: 100
omit-unchanged: true
diff --git a/.github/workflows/pr-build-live-branch.yml b/.github/workflows/pr-build-live-branch.yml
index d43fc964500..42c39a49c93 100644
--- a/.github/workflows/pr-build-live-branch.yml
+++ b/.github/workflows/pr-build-live-branch.yml
@@ -8,6 +8,8 @@ on:
- '**/changelog/**'
- '**/tests/**'
- '**/*.md'
+ - '.github/**'
+ - '!.github/workflows/pr-build-live-branch.yml'
concurrency:
# Cancel concurrent jobs on pull_request but not push, by including the run_id in the concurrency group for the latter.
@@ -39,12 +41,8 @@ jobs:
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
-
- - name: Cache PNPM Dependencies
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65
with:
- node-version-file: .nvmrc
- cache: pnpm
+ pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Prepare plugin zips
id: prepare
diff --git a/.github/workflows/pr-highlight-changes.yml b/.github/workflows/pr-highlight-changes.yml
index 6ae2428a981..3fd276e222a 100644
--- a/.github/workflows/pr-highlight-changes.yml
+++ b/.github/workflows/pr-highlight-changes.yml
@@ -17,6 +17,8 @@ jobs:
with:
install: 'code-analyzer...'
build: 'code-analyzer'
+ pull-package-deps: 'code-analyzer'
+
- name: 'Analyze'
id: 'analyze'
working-directory: 'tools/code-analyzer'
diff --git a/.github/workflows/prepare-package-release.yml b/.github/workflows/prepare-package-release.yml
index 237b49afbdd..d2c735eccce 100644
--- a/.github/workflows/prepare-package-release.yml
+++ b/.github/workflows/prepare-package-release.yml
@@ -26,6 +26,7 @@ jobs:
with:
install: true
build: './tools/package-release'
+ pull-package-deps: 'tools/package-release'
- name: Execute script
run: ./tools/package-release/bin/dev prepare ${{ github.event.inputs.packages }}
diff --git a/.github/workflows/release-code-freeze.yml b/.github/workflows/release-code-freeze.yml
index 5a4b87290ec..2605eb5cce4 100644
--- a/.github/workflows/release-code-freeze.yml
+++ b/.github/workflows/release-code-freeze.yml
@@ -191,6 +191,8 @@ jobs:
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
+ with:
+ pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Build zip
working-directory: plugins/woocommerce
@@ -219,6 +221,8 @@ jobs:
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
+ with:
+ pull-package-deps: '@woocommerce/plugin-woocommerce'
- name: Build zip
working-directory: plugins/woocommerce
diff --git a/.github/workflows/release-wc-beta-tester.yml b/.github/workflows/release-wc-beta-tester.yml
index 55ccfece4a3..f11c1c9d27b 100644
--- a/.github/workflows/release-wc-beta-tester.yml
+++ b/.github/workflows/release-wc-beta-tester.yml
@@ -22,6 +22,7 @@ jobs:
with:
install: '@woocommerce/plugin-woocommerce'
build: '@woocommerce/plugin-woocommerce'
+ pull-package-deps: '@woocommerce/plugin-woocommerce-beta-tester'
- name: Lint
working-directory: plugins/woocommerce-beta-tester
diff --git a/.github/workflows/review-testing-instructions.yml b/.github/workflows/review-testing-instructions.yml
index 9bd6c292eae..ff86c2ff665 100644
--- a/.github/workflows/review-testing-instructions.yml
+++ b/.github/workflows/review-testing-instructions.yml
@@ -1,66 +1,70 @@
-name: Remind reviewers to also review the testing instructions.
+name: Remind reviewers to also review the testing instructions and test coverage
on:
- pull_request_target:
- types: [review_requested]
+ pull_request_target:
+ types: [review_requested]
permissions: {}
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
jobs:
- add-testing-instructions-review-comment:
- runs-on: ubuntu-20.04
- permissions:
- pull-requests: write
- steps:
- - uses: actions/checkout@v3
+ add-testing-instructions-review-comment:
+ runs-on: ubuntu-20.04
+ permissions:
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@v3
- - name: Setup Node.js
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
+ - name: Setup Node.js
+ uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
- - name: Install Octokit
- run: npm --prefix .github/workflows/scripts install @octokit/action@~6.1.0
+ - name: Install Octokit
+ run: npm --prefix .github/workflows/scripts install @octokit/action@~6.1.0
- - name: Install Actions Core
- run: npm --prefix .github/workflows/scripts install @actions/core@~1.10.1
+ - name: Install Actions Core
+ run: npm --prefix .github/workflows/scripts install @actions/core@~1.10.1
- - name: Check if user is a community contributor
- id: is-community-contributor
- run: node .github/workflows/scripts/is-community-contributor.js
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Check if user is a community contributor
+ id: is-community-contributor
+ run: node .github/workflows/scripts/is-community-contributor.js
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Get the username of requested reviewers
- if: steps.is-community-contributor.outputs.is-community == 'no'
- id: get_reviewer_username
- run: |
- # Retrieves the username of all reviewers and stores them in a comma-separated list
- reviewers=$(echo '${{ toJson(github.event.pull_request.requested_reviewers[*].login) }}' | jq -r 'map("@\(.)") | join(", ")')
- echo "REVIEWERS=$reviewers" >> $GITHUB_ENV
+ - name: Get the username of requested reviewers
+ if: steps.is-community-contributor.outputs.is-community == 'no'
+ id: get_reviewer_username
+ run: |
+ # Retrieves the username of all reviewers and stores them in a comma-separated list
+ reviewers=$(echo '${{ toJson(github.event.pull_request.requested_reviewers[*].login) }}' | jq -r 'map("@\(.)") | join(", ")')
+ echo "REVIEWERS=$reviewers" >> $GITHUB_ENV
- - name: Get the name of requested teams
- id: get_team_name
- run: |
- # Retrieves the name of all teams asked for review and stores them in a comma-separated list
- teams=$(echo '${{ toJson(github.event.pull_request.requested_teams[*].slug) }}' | jq -r 'map("@woocommerce/\(.)") | join(", ")')
- echo "TEAMS=$teams" >> $GITHUB_ENV
+ - name: Get the name of requested teams
+ id: get_team_name
+ run: |
+ # Retrieves the name of all teams asked for review and stores them in a comma-separated list
+ teams=$(echo '${{ toJson(github.event.pull_request.requested_teams[*].slug) }}' | jq -r 'map("@woocommerce/\(.)") | join(", ")')
+ echo "TEAMS=$teams" >> $GITHUB_ENV
- - name: Find the comment by github-actions[bot] asking for reviewing the testing instructions
- uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb
- id: find-comment
- with:
- issue-number: ${{ github.event.pull_request.number }}
- comment-author: 'github-actions[bot]'
- body-includes: please make sure to review the testing instructions
+ - name: Find the comment by github-actions[bot] asking for reviewing the testing instructions
+ uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb
+ id: find-comment
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-author: 'github-actions[bot]'
+ body-includes: please make sure to review the testing instructions
- - name: Create or update PR comment asking for reviewers to review the testing instructions
- uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d
- with:
- comment-id: ${{ steps.find-comment.outputs.comment-id }}
- issue-number: ${{ github.event.pull_request.number }}
- body: |
- Hi ${{ env.REVIEWERS }}, ${{ env.TEAMS }}
-
- Apart from reviewing the code changes, please make sure to review the testing instructions as well.
-
- You can follow this guide to find out what good testing instructions should look like:
- https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions
- edit-mode: replace
+ - name: Create or update PR comment asking for reviewers to review the testing instructions and test coverage
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d
+ with:
+ comment-id: ${{ steps.find-comment.outputs.comment-id }}
+ issue-number: ${{ github.event.pull_request.number }}
+ body: |
+ Hi ${{ env.REVIEWERS }}, ${{ env.TEAMS }}
+
+ Apart from reviewing the code changes, please make sure to review the testing instructions and verify that relevant tests (E2E, Unit, Integration, etc.) have been added or updated as needed.
+
+ You can follow this guide to find out what good testing instructions should look like:
+ https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions
+ edit-mode: replace
diff --git a/.github/workflows/scripts/override-wp-env-plugins.js b/.github/workflows/scripts/override-wp-env-plugins.js
new file mode 100644
index 00000000000..33f05c9569b
--- /dev/null
+++ b/.github/workflows/scripts/override-wp-env-plugins.js
@@ -0,0 +1,69 @@
+/* eslint-disable no-console */
+const fs = require( 'fs' );
+
+const { RELEASE_TAG, ARTIFACT_NAME, WP_ENV_CONFIG_PATH } = process.env;
+
+if ( ! RELEASE_TAG ) {
+ console.error( 'Please set the RELEASE_TAG environment variable!' );
+ process.exit( 1 );
+}
+
+if ( ! ARTIFACT_NAME ) {
+ console.error( 'Please set the ARTIFACT_NAME environment variable!' );
+ process.exit( 1 );
+}
+
+if ( ! WP_ENV_CONFIG_PATH ) {
+ console.error( 'Please set the WP_ENV_CONFIG_PATH environment variable!' );
+ process.exit( 1 );
+}
+
+const artifactUrl = `https://github.com/woocommerce/woocommerce/releases/download/${ RELEASE_TAG }/${ ARTIFACT_NAME }`;
+
+const configPath = `${ WP_ENV_CONFIG_PATH }/.wp-env.json`;
+console.log( `Reading ${ configPath }` );
+const data = fs.readFileSync( configPath, 'utf8' );
+const wpEnvConfig = JSON.parse( data );
+
+const overrideConfig = {};
+
+if ( wpEnvConfig.plugins ) {
+ overrideConfig.plugins = wpEnvConfig.plugins;
+}
+
+if ( wpEnvConfig.env?.tests?.plugins ) {
+ overrideConfig.env = {
+ tests: {
+ plugins: wpEnvConfig.env.tests.plugins,
+ },
+ };
+}
+
+const entriesToReplace = [ '.', '../woocommerce' ];
+
+for ( const entry of entriesToReplace ) {
+ // Search and replace in root plugins
+ let found = overrideConfig.plugins.indexOf( entry );
+ if ( found >= 0 ) {
+ console.log(
+ `Replacing ${ entry } with ${ artifactUrl } in root plugins`
+ );
+ overrideConfig.plugins[ found ] = artifactUrl;
+ }
+
+ // Search and replace in test env plugins
+ found = overrideConfig.env?.tests?.plugins?.indexOf( entry );
+ if ( found >= 0 ) {
+ console.log(
+ `Replacing ${ entry } with ${ artifactUrl } in env.tests.plugins`
+ );
+ overrideConfig.env.tests.plugins[ found ] = artifactUrl;
+ }
+}
+
+const overrideConfigPath = `${ WP_ENV_CONFIG_PATH }/.wp-env.override.json`;
+console.log( `Saving ${ overrideConfigPath }` );
+fs.writeFileSync(
+ overrideConfigPath,
+ JSON.stringify( overrideConfig, null, 2 )
+);
diff --git a/.github/workflows/tests-daily-run.yml b/.github/workflows/tests-daily-run.yml
index 581ec84543e..ccea88a5620 100644
--- a/.github/workflows/tests-daily-run.yml
+++ b/.github/workflows/tests-daily-run.yml
@@ -7,6 +7,7 @@ on:
jobs:
run-tests:
name: 'Run tests'
+ if: github.repository == 'woocommerce/woocommerce'
uses: ./.github/workflows/ci.yml
with:
trigger: 'daily-checks'
diff --git a/.github/workflows/tests-on-demand.yml b/.github/workflows/tests-on-demand.yml
new file mode 100644
index 00000000000..c558dec65a4
--- /dev/null
+++ b/.github/workflows/tests-on-demand.yml
@@ -0,0 +1,38 @@
+name: 'On demand tests run'
+
+on:
+ workflow_dispatch:
+ inputs:
+ trigger:
+ type: choice
+ description: 'Event name: it will be used to filter the jobs to run in ci.yml.'
+ required: true
+ options:
+ - push
+ - daily-checks
+ - pre-release
+ - on-demand
+ - custom
+ default: on-demand
+ custom-trigger:
+ type: string
+ description: 'Custom event name: In case the `Event name` choice is `custom`, this field is required.'
+ required: false
+
+jobs:
+ validate-input:
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Validate input'
+ run: |
+ if [ "${{ inputs.trigger }}" == "custom" ] && [ -z "${{ inputs.custom-trigger }}" ]; then
+ echo "Custom event name is required when event name choice `custom`."
+ exit 1
+ fi
+
+ run-tests:
+ name: 'Run tests'
+ uses: ./.github/workflows/ci.yml
+ with:
+ trigger: ${{ inputs.trigger == 'custom' && inputs.custom-trigger || inputs.trigger }}
+ secrets: inherit
diff --git a/.syncpackrc b/.syncpackrc
index 663adf46bcd..d82f3b8375e 100644
--- a/.syncpackrc
+++ b/.syncpackrc
@@ -41,7 +41,43 @@
"@types/*",
"@typescript-eslint/*",
"@woocommerce/*",
- "@wordpress/*",
+ "@wordpress/api-fetch",
+ "@wordpress/autop",
+ "@wordpress/babel-preset-default",
+ "@wordpress/base-styles",
+ "@wordpress/block-editor",
+ "@wordpress/blocks",
+ "@wordpress/browserslist-config",
+ "@wordpress/components",
+ "@wordpress/compose",
+ "@wordpress/core-data",
+ "@wordpress/data",
+ "@wordpress/data-controls",
+ "@wordpress/date",
+ "@wordpress/dependency-extraction-webpack-plugin",
+ "@wordpress/deprecated",
+ "@wordpress/dom",
+ "@wordpress/dom-ready",
+ "@wordpress/e2e-test-utils",
+ "@wordpress/e2e-test-utils-playwright",
+ "@wordpress/e2e-tests",
+ "@wordpress/element",
+ "@wordpress/html-entities",
+ "@wordpress/i18n",
+ "@wordpress/icons",
+ "@wordpress/is-shallow-equal",
+ "@wordpress/notices",
+ "@wordpress/plugins",
+ "@wordpress/postcss-plugins-preset",
+ "@wordpress/postcss-themes",
+ "@wordpress/prettier-config",
+ "@wordpress/primitives",
+ "@wordpress/scripts",
+ "@wordpress/server-side-render",
+ "@wordpress/style-engine",
+ "@wordpress/stylelint-config",
+ "@wordpress/url",
+ "@wordpress/wordcount",
"babel*",
"eslint*",
"glob*",
@@ -205,9 +241,10 @@
"@wordpress/env"
],
"packages": [
+ "@woocommerce/block-library",
"**"
],
- "pinVersion": "^9.0.7"
+ "pinVersion": "^9.7.0"
},
{
"dependencies": [
diff --git a/bin/patches/@wordpress__edit-site@5.15.0.patch b/bin/patches/@wordpress__edit-site@5.15.0.patch
new file mode 100644
index 00000000000..ca97ce333e0
--- /dev/null
+++ b/bin/patches/@wordpress__edit-site@5.15.0.patch
@@ -0,0 +1,44 @@
+diff --git a/build-module/lock-unlock.js b/build-module/lock-unlock.js
+index 2265f933ceec19f65ca6776c24c3f88b368d713f..e9e10980bfd1b584ab0a037c3b72edae29a2a26e 100644
+--- a/build-module/lock-unlock.js
++++ b/build-module/lock-unlock.js
+@@ -1,9 +1,34 @@
+ /**
+- * WordPress dependencies
++ * External dependencies
+ */
+ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
+-export const {
+- lock,
+- unlock
+-} = __dangerousOptInToUnstableAPIsOnlyForCoreModules('I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.', '@wordpress/edit-site');
++
++// Workaround for Gutenberg private API consent string differences between WP 6.3 and 6.4+
++// The modified version checks for the WP version and replaces the consent string with the correct one.
++// This can be removed once we drop support for WP 6.3 in the "Customize Your Store" task.
++// See this PR for details: https://github.com/woocommerce/woocommerce/pull/40884
++
++const wordPressConsentString = {
++ 6.4: 'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.',
++ 6.5: 'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.',
++ 6.6: 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
++};
++
++function optInToUnstableAPIs() {
++ let error;
++ for ( const optInString of Object.values( wordPressConsentString ) ) {
++ try {
++ return __dangerousOptInToUnstableAPIsOnlyForCoreModules(
++ optInString,
++ '@wordpress/edit-site'
++ );
++ } catch ( anError ) {
++ error = anError;
++ }
++ }
++
++ throw error;
++}
++
++export const { lock, unlock } = optInToUnstableAPIs();
+ //# sourceMappingURL=lock-unlock.js.map
diff --git a/changelog.txt b/changelog.txt
index 497385fc2a7..01b8c0ed896 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,5 +1,255 @@
== Changelog ==
+= 9.1.4 2024-07-26 =
+
+**WooCommerce**
+
+* Fix - Revert fixing terms count in tracking PR as it caused product_add_publish to be triggered more than usual. [#49797](https://github.com/woocommerce/woocommerce/pull/49797)
+* Fix - Hardening against XSS via the Product Button unescaped attribute. [#50010](https://github.com/woocommerce/woocommerce/pull/50010)
+* Fix - Enhance escaping for block attributes. [#50015](https://github.com/woocommerce/woocommerce/pull/50015)
+
+
+= 9.1.2 2024-07-12 =
+
+**WooCommerce**
+
+* Fix - Revert 46857 to preserve backcompat with earlier WC versions. [#48753](https://github.com/woocommerce/woocommerce/pull/48753)
+
+= 9.1.1 2024-07-11 =
+
+**WooCommerce**
+
+* Tweak - Revert #46262, as that PR would render input values invisible under certain conditions. [49404](https://github.com/woocommerce/woocommerce/pull/49404)
+
+= 9.1.0 2024-07-10 =
+
+**WooCommerce**
+
+* Fix - Prevent HTML tags being rendered on order confirmation and emails [#49370](https://github.com/woocommerce/woocommerce/pull/49370)
+* Security - Improve the way we cache information about recent customer activity, to prevent the wrong data being retrieved in some specific conditions involving multisite networks. [#49373](https://github.com/woocommerce/woocommerce/pull/49373)
+* Fix - Prevent BatchProcessingController from cleaning up processors after a premature shutdown. [#49243](https://github.com/woocommerce/woocommerce/pull/49243)
+* Fix - CYS: fix not template set correctly. [#49113](https://github.com/woocommerce/woocommerce/pull/49113)
+* Fix - CYS: Disable readonly mode only when full composability feature flag is enabled. [#48752](https://github.com/woocommerce/woocommerce/pull/48752)
+* Fix - CYS: fix crash of CYS on WordPress 6.6 [#48664](https://github.com/woocommerce/woocommerce/pull/48664)
+* Fix - Revert "Set stock quantity value as 0 by default (#48448)" #48863 [#48863](https://github.com/woocommerce/woocommerce/pull/48863)
+* Fix - Add product id to product_edit_view track in classic product edit screen [#47853](https://github.com/woocommerce/woocommerce/pull/47853)
+* Fix - Address responsiveness issues in orders list table. [#47684](https://github.com/woocommerce/woocommerce/pull/47684)
+* Fix - Add screen-reader-text styles to e-mails. [#47738](https://github.com/woocommerce/woocommerce/pull/47738)
+* Fix - Adds new hook `woocommerce_rest_delete_shipping_zone_method` which will fire after a shipping zone method is deleted via the REST API. [#47862](https://github.com/woocommerce/woocommerce/pull/47862)
+* Fix - Allow products with non-integer stock to be created via REST API. [#48541](https://github.com/woocommerce/woocommerce/pull/48541)
+* Fix - Calling $product->get_status() after $product->save() on a new product now returns correct status. [#48241](https://github.com/woocommerce/woocommerce/pull/48241)
+* Fix - Change the cursor to a pointer when hovering over the mini cart [#46996](https://github.com/woocommerce/woocommerce/pull/46996)
+* Fix - CYS - Hovering over the header or footer on the "Design your homepage" section should not make them highlighted. [#48358](https://github.com/woocommerce/woocommerce/pull/48358)
+* Fix - CYS - Select the next block after deleting the selected one (instead of the header). [#48316](https://github.com/woocommerce/woocommerce/pull/48316)
+* Fix - CYS: apply white color to the heading elements in the core/cover block. [#48447](https://github.com/woocommerce/woocommerce/pull/48447)
+* Fix - CYS: Fix crash homepage. [#48205](https://github.com/woocommerce/woocommerce/pull/48205)
+* Fix - CYS: Fix CSS header. [#48389](https://github.com/woocommerce/woocommerce/pull/48389)
+* Fix - CYS: fix logic to disable mover buttons. [#48502](https://github.com/woocommerce/woocommerce/pull/48502)
+* Fix - CYS: fix tooltip position. [#48495](https://github.com/woocommerce/woocommerce/pull/48495)
+* Fix - CYS: hide popover when the mouse pointer leaves the site preview and then back. [#48394](https://github.com/woocommerce/woocommerce/pull/48394)
+* Fix - Do not create empty webhooks after failure to deliver deleted webhook. [#48480](https://github.com/woocommerce/woocommerce/pull/48480)
+* Fix - Ensure attribute slugs with multibyte characters are handled property when outputting attributes in the REST API products endpoint [#48198](https://github.com/woocommerce/woocommerce/pull/48198)
+* Fix - Ensure available stock is updated correctly when updating line items in orders via the REST API. [#47784](https://github.com/woocommerce/woocommerce/pull/47784)
+* Fix - Ensure data filtered by `woocommerce_logger_log_message` does not carry across multiple log handlers [#48336](https://github.com/woocommerce/woocommerce/pull/48336)
+* Fix - Ensure getPreviousDate default behaviour is comparing previous_year [#47951](https://github.com/woocommerce/woocommerce/pull/47951)
+* Fix - Ensure permission checks for the customer downloads REST API endpoint use the correct customer ID. [#47854](https://github.com/woocommerce/woocommerce/pull/47854)
+* Fix - Ensure that data containing multibyte characters and/or slashes that is appended to log entries gets encoded and rendered correctly [#48341](https://github.com/woocommerce/woocommerce/pull/48341)
+* Fix - Fix a bug with the woocommerce_get_default_value_for_{key} filter that was preventing setting a falsey value on a checkbox (i.e. to uncheck it dynamically) [#48031](https://github.com/woocommerce/woocommerce/pull/48031)
+* Fix - Fix activation limit for single license subscriptions on woocommerce.com [#47643](https://github.com/woocommerce/woocommerce/pull/47643)
+* Fix - Fix a null parameter being passed into strpos in Admin/Orders/PageController.php [#48476](https://github.com/woocommerce/woocommerce/pull/48476)
+* Fix - Fix bug where Core Profiler initiates a Jetpack connection even if it was already connected before [#48345](https://github.com/woocommerce/woocommerce/pull/48345)
+* Fix - Fix bumped down data when analytics chart current period contains 29th Feb [#45874](https://github.com/woocommerce/woocommerce/pull/45874)
+* Fix - Fix coming soon footer banner doesn't display properly on tablet and mobile [#47980](https://github.com/woocommerce/woocommerce/pull/47980)
+* Fix - Fix e2e tests about the tabs selection during the product creation experience [#47860](https://github.com/woocommerce/woocommerce/pull/47860)
+* Fix - Fix edit variable product test [#48288](https://github.com/woocommerce/woocommerce/pull/48288)
+* Fix - Fix FlexSlider thumbnail animation for variable products with default form values on small devices. [#48137](https://github.com/woocommerce/woocommerce/pull/48137)
+* Fix - Fix location settings not updated in tax task [#48606](https://github.com/woocommerce/woocommerce/pull/48606)
+* Fix - Fix LYS private link URL parameter regardless of permalink settings [#48425](https://github.com/woocommerce/woocommerce/pull/48425)
+* Fix - Fix product archive page not hidden behind the coming soon page [#48522](https://github.com/woocommerce/woocommerce/pull/48522)
+* Fix - Fix Product Gallery block error on revisiting Single Product template without fully reloading the page. [#47636](https://github.com/woocommerce/woocommerce/pull/47636)
+* Fix - Fix product tracks when importing #47857 [#47857](https://github.com/woocommerce/woocommerce/pull/47857)
+* Fix - Fix some issues in performance tests #47735 [#47735](https://github.com/woocommerce/woocommerce/pull/47735)
+* Fix - Fix the issue that the React-powered admin routing pages added after the filter initialization could not be displayed. [#47696](https://github.com/woocommerce/woocommerce/pull/47696)
+* Fix - Fix the terms counts in wcadmin_product_add_publish event. [#48194](https://github.com/woocommerce/woocommerce/pull/48194)
+* Fix - Fix two products being added to cart when Geolocate (with page caching support) was enabled and AJAX add to cart buttons disabled [#47761](https://github.com/woocommerce/woocommerce/pull/47761)
+* Fix - Fix untranslated strings on CYS and marketplace [#48127](https://github.com/woocommerce/woocommerce/pull/48127)
+* Fix - Honor empty "additional content" setting in e-mails. [#47809](https://github.com/woocommerce/woocommerce/pull/47809)
+* Fix - Improve consistency of Setting-> Gateway Manage button for WooPayments gateway [#48212](https://github.com/woocommerce/woocommerce/pull/48212)
+* Fix - In general, the `last_access` field of a REST API key should only be updated once-per-request.
+* Fix - Make coupon metadata read robust against wrongly stored product related metadata [#48362](https://github.com/woocommerce/woocommerce/pull/48362)
+* Fix - Moved WooCommerce block categories registration on the server-side, fixing a bug that would show warnings to developers trying to hook new blocks in such categories. [#47836](https://github.com/woocommerce/woocommerce/pull/47836)
+* Fix - Possible availability of unpublished coupons on sites with an object cache has been addressed through improved cache management. [#47739](https://github.com/woocommerce/woocommerce/pull/47739)
+* Fix - Prefer update URLs over PluginURI in My Subscriptions for plugins without a subscription. [#47950](https://github.com/woocommerce/woocommerce/pull/47950)
+* Fix - Prevent on-sale badge from showing on top of the coming soon banner. [#48082](https://github.com/woocommerce/woocommerce/pull/48082)
+* Fix - Prevent Product Gallery from being inserted on Posts and Pages. [#48228](https://github.com/woocommerce/woocommerce/pull/48228)
+* Fix - Product Collection: prevent throwing warnings in some circumstances when rendering block [#48530](https://github.com/woocommerce/woocommerce/pull/48530)
+* Fix - Product Price: Narrow down the ancestors of the block so it's available in inserter only in places where block makes sense [#47802](https://github.com/woocommerce/woocommerce/pull/47802)
+* Fix - Re-enable variable product E2E test #48294 [#48294](https://github.com/woocommerce/woocommerce/pull/48294)
+* Fix - Related Products: hides unusable options from Inspector Controls [#47845](https://github.com/woocommerce/woocommerce/pull/47845)
+* Fix - Run possibly_add_template_id function in woocommerce_rest_prepare_product_variation_object hook [#48325](https://github.com/woocommerce/woocommerce/pull/48325)
+* Fix - Scroll to view the templates section on the status page [#48125](https://github.com/woocommerce/woocommerce/pull/48125)
+* Fix - Set stock quantity value as 0 by default #48448 [#48448](https://github.com/woocommerce/woocommerce/pull/48448)
+* Fix - Update plugin installation error logger to use plugin track key for extension name [#47786](https://github.com/woocommerce/woocommerce/pull/47786)
+* Fix - When a product attribute is updated, unchanged values should not be reset to their defaults. [#48120](https://github.com/woocommerce/woocommerce/pull/48120)
+* Fix - WooCommerce: fixes the checks when migrating the product form template [#48386](https://github.com/woocommerce/woocommerce/pull/48386)
+* Fix - [CYS Full Composability] Ensure that the assembler doesn't crash when the feature flag is enabled, but the site doesn't have the latest version of Gutenberg. [#47546](https://github.com/woocommerce/woocommerce/pull/47546)
+* Add - Add CLI tools for the product attributes lookup table [#47311](https://github.com/woocommerce/woocommerce/pull/47311)
+* Add - Add 'woocommerce_order_note_deleted' hook for order note deletions. [#47916](https://github.com/woocommerce/woocommerce/pull/47916)
+* Add - Add CLI tools to enable and disable HPOS compatibility mode. [#48117](https://github.com/woocommerce/woocommerce/pull/48117)
+* Add - Added 'woocommerce_restore_order_item_stock' filter for restored line item stock on canceled orders [#40848](https://github.com/woocommerce/woocommerce/pull/40848)
+* Add - Add ErrorBoundary component for handling unexpect errors [#48250](https://github.com/woocommerce/woocommerce/pull/48250)
+* Add - Add filter to dynamically exclude a page from Coming soon mode [#47787](https://github.com/woocommerce/woocommerce/pull/47787)
+* Add - Add Printful product placement to Add Products task [#48520](https://github.com/woocommerce/woocommerce/pull/48520)
+* Add - Add skipped test custom reporter to surface skipped tests in CI runs [#48195](https://github.com/woocommerce/woocommerce/pull/48195)
+* Add - Add the ability to test experimental blocks via the Advanced > Features menu of WooCommerce settings. [#47701](https://github.com/woocommerce/woocommerce/pull/47701)
+* Add - Add woocommerce_manage_stock option to the default_option_permissions list in the Options rest controller [#48239](https://github.com/woocommerce/woocommerce/pull/48239)
+* Add - CYS: add CTA to our Fiverr Logo Maker landing page. [#48486](https://github.com/woocommerce/woocommerce/pull/48486)
+* Add - CYS: add pattern category in the block toolbar. [#48501](https://github.com/woocommerce/woocommerce/pull/48501)
+* Add - CYS: Add the Delete button to the Block Toolbar. [#48143](https://github.com/woocommerce/woocommerce/pull/48143)
+* Add - CYS: Ensure that toolbar appears only when the homepage sidebar is open. [#48115](https://github.com/woocommerce/woocommerce/pull/48115)
+* Add - CYS: Show Patterns from PTK. [#48207](https://github.com/woocommerce/woocommerce/pull/48207)
+* Add - CYS: Show popover when the user clicks on the pattern [#47583](https://github.com/woocommerce/woocommerce/pull/47583)
+* Add - Determine _product_template_id from 'woocommerce_product_editor_determine_product_template' filter [#47762](https://github.com/woocommerce/woocommerce/pull/47762)
+* Add - Display an admin notice in Setting and Extension pages when there are expiring subscriptions and connected account doesn't have a payment method. [#47141](https://github.com/woocommerce/woocommerce/pull/47141)
+* Add - Enhancements to background batch processing. [#48078](https://github.com/woocommerce/woocommerce/pull/48078)
+* Add - Highlight the pattern when the user hovers it. [#47415](https://github.com/woocommerce/woocommerce/pull/47415)
+* Add - LYS - Add 'Remove test orders' for WooPayments [#47832](https://github.com/woocommerce/woocommerce/pull/47832)
+* Add - PFT: introduce controller and initialize it [#48221](https://github.com/woocommerce/woocommerce/pull/48221)
+* Add - REST API: extened shipping_classes namespace with the /suggest-slug endpoint [#47896](https://github.com/woocommerce/woocommerce/pull/47896)
+* Add - Updated shipstation copy [#48549](https://github.com/woocommerce/woocommerce/pull/48549)
+* Add - WooCommerce: create a new product_form CPT [#48073](https://github.com/woocommerce/woocommerce/pull/48073)
+* Add - WooCommerce: introduce `product-editor-template-system` feature flag [#48136](https://github.com/woocommerce/woocommerce/pull/48136)
+* Add - WooCommerce: update CPT product_form posts when plugin updates [#48265](https://github.com/woocommerce/woocommerce/pull/48265)
+* Add - WooCommerce Blocks: Added a GitHub Action to create issues for flaky E2E tests [#47758](https://github.com/woocommerce/woocommerce/pull/47758)
+* Update - Add feature flag for Printful placement [#49104](https://github.com/woocommerce/woocommerce/pull/49104)
+* Update - Add a control to enable a separator on the Checkout block's "Checkout Terms" block. This will enable a separator above the block that can be turned off in case the block is moved. [#47565](https://github.com/woocommerce/woocommerce/pull/47565)
+* Update - Change the item schemas for Orders and Order Refunds API endpoints to correctly specify that the rate_id property in a tax_line object is an integer, not a string [#47779](https://github.com/woocommerce/woocommerce/pull/47779)
+* Update - Clean up theming sections in WooCommerce blocks docs [#48420](https://github.com/woocommerce/woocommerce/pull/48420)
+* Update - CYS - Exclude two testimonials patterns from registering since they depend on Jetpack. [#48233](https://github.com/woocommerce/woocommerce/pull/48233)
+* Update - CYS - Fix active/inactive patterns for each of the sections in the assembler. [#48458](https://github.com/woocommerce/woocommerce/pull/48458)
+* Update - CYS - Install the patterns during the CYS flow if the transient is not set. [#48274](https://github.com/woocommerce/woocommerce/pull/48274)
+* Update - CYS - Redirect to the same section after installing fonts or patterns on the assembler. [#48227](https://github.com/woocommerce/woocommerce/pull/48227)
+* Update - CYS - Show tooltips on the Shuffle and Delete buttons in the assembler toolbar. [#48465](https://github.com/woocommerce/woocommerce/pull/48465)
+* Update - CYS: set new default patterns. [#48467](https://github.com/woocommerce/woocommerce/pull/48467)
+* Update - Display return to cart link on mobile devices. [#48103](https://github.com/woocommerce/woocommerce/pull/48103)
+* Update - Docs: update documentation regarding Compatibility Layer [#48456](https://github.com/woocommerce/woocommerce/pull/48456)
+* Update - Expand block templates documentation [#48247](https://github.com/woocommerce/woocommerce/pull/48247)
+* Update - Experimental blocks now have "(Experimental)" suffix [#48071](https://github.com/woocommerce/woocommerce/pull/48071)
+* Update - fix: label improvement on my order page template [#48374](https://github.com/woocommerce/woocommerce/pull/48374)
+* Update - Improve WooCommerce block template names in the Add New Template screen. [#48106](https://github.com/woocommerce/woocommerce/pull/48106)
+* Update - Invalidate cache for SiteGround Speed Optimizer [#48523](https://github.com/woocommerce/woocommerce/pull/48523)
+* Update - Optimize the regeneration of the product attributes lookup table [#47700](https://github.com/woocommerce/woocommerce/pull/47700)
+* Update - Product Archive templates: Replace the default block from Products (Beta) to Product Collection block [#48112](https://github.com/woocommerce/woocommerce/pull/48112)
+* Update - Product Block Editor: disable the `product-editor-template-system` feature flag as default, even for the development environment. [#48378](https://github.com/woocommerce/woocommerce/pull/48378)
+* Update - Product Collection: Handpicked Products filter now allows searching from 2 characters and more and updates available results as you type [#48379](https://github.com/woocommerce/woocommerce/pull/48379)
+* Update - Product Elements: hide Product Summary from Single Product block and only show Excerpt variation [#48253](https://github.com/woocommerce/woocommerce/pull/48253)
+* Update - Product Rating Stars and Product Rating Counter from the inserter [#48229](https://github.com/woocommerce/woocommerce/pull/48229)
+* Update - Products (Beta): hide block from inserter in favor of Product Collection block [#48204](https://github.com/woocommerce/woocommerce/pull/48204)
+* Update - Product Summary: Increase the length of the description from 55 to 100 words (max supported by core/post-excerpt) [#47651](https://github.com/woocommerce/woocommerce/pull/47651)
+* Update - Reduced the number of FlexSlider animation engines from 2 to 1, now always using CSS3 transitions. [#46564](https://github.com/woocommerce/woocommerce/pull/46564)
+* Update - Replace the use of options endpoint with the LYS API endpoint to query woocommerce_admin_launch_your_store_survey_completed option. [#47915](https://github.com/woocommerce/woocommerce/pull/47915)
+* Update - The archive product title will now be updated to the title of the current shop
+ page. If the page does not exist, it will fall back to "Shop". [#48255](https://github.com/woocommerce/woocommerce/pull/48255)
+* Update - Toggle LYS feature flag on for 9.1 [#48244](https://github.com/woocommerce/woocommerce/pull/48244)
+* Update - Update input fields styles of the Checkout block [#46362](https://github.com/woocommerce/woocommerce/pull/46362)
+* Update - WooCommerce: store the template description in the `product_form` excerpt property. [#48327](https://github.com/woocommerce/woocommerce/pull/48327)
+* Update - Wrap activity panels in error boundary [#48415](https://github.com/woocommerce/woocommerce/pull/48415)
+* Update - [CYS] Ensure fetch PTK patterns requests are always done async to improve performance. [#47551](https://github.com/woocommerce/woocommerce/pull/47551)
+* Update - [CYS] Refactor the pattern registration and add patterns from the PTK API. [#47306](https://github.com/woocommerce/woocommerce/pull/47306)
+* Update - [CYS] Remove the restriction to TT4 and allow users to proceed to the pattern assembler with any block themes. Update intro page design. [#46916](https://github.com/woocommerce/woocommerce/pull/46916)
+* Update - [CYS] Show a message when tracking is not allowed in patterns and add the ability for users to opt-in and fetch patterns. [#48095](https://github.com/woocommerce/woocommerce/pull/48095)
+* Dev - Improve E2E selector by making it stricter. Wait for text due to AJAX call. [#48471](https://github.com/woocommerce/woocommerce/pull/48471)
+* Dev - Added e2e test to check ability to connect to woocommerce.com [#48028](https://github.com/woocommerce/woocommerce/pull/48028)
+* Dev - Added test enviornments [#48101](https://github.com/woocommerce/woocommerce/pull/48101)
+* Dev - Add previous error class to checkout endpoint response [#47489](https://github.com/woocommerce/woocommerce/pull/47489)
+* Dev - Add test for wcpay_connect_account_clicked track [#48347](https://github.com/woocommerce/woocommerce/pull/48347)
+* Dev - Add tests for some product editor tracks [#48245](https://github.com/woocommerce/woocommerce/pull/48245)
+* Dev - Blocks E2E: Remove confusing utilities in favor of native locator functionality. [#47904](https://github.com/woocommerce/woocommerce/pull/47904)
+* Dev - CI: merge test jobs [#48175](https://github.com/woocommerce/woocommerce/pull/48175)
+* Dev - Clean up eslint comments after rules update in Blocks E2E tests. [#47875](https://github.com/woocommerce/woocommerce/pull/47875)
+* Dev - Clean up tasklist progression headercard experiment [#47983](https://github.com/woocommerce/woocommerce/pull/47983)
+* Dev - Clean up welcome modal code [#48346](https://github.com/woocommerce/woocommerce/pull/48346)
+* Dev - Do not dismiss the error snackbar automatically, fix E2E test #48192 [#48192](https://github.com/woocommerce/woocommerce/pull/48192)
+* Dev - E2E test: Improve analytics data spec by disabling the task list reminder bar [#48357](https://github.com/woocommerce/woocommerce/pull/48357)
+* Dev - E2E tests: configure snapshotPathTemplate [#47773](https://github.com/woocommerce/woocommerce/pull/47773)
+* Dev - E2E tests: fixing flakiness in checkout block and launch your store tests [#48016](https://github.com/woocommerce/woocommerce/pull/48016)
+* Dev - E2E tests: fixing flaky assembler homepage test [#48356](https://github.com/woocommerce/woocommerce/pull/48356)
+* Dev - E2E tests: fixing flaky checkout block test [#48527](https://github.com/woocommerce/woocommerce/pull/48527)
+* Dev - E2E tests: fixing flaky color palette picker test [#48496](https://github.com/woocommerce/woocommerce/pull/48496)
+* Dev - E2E tests: fixing flaky connect to woo test [#48613](https://github.com/woocommerce/woocommerce/pull/48613)
+* Dev - E2E tests: fixing flaky customize store transitional test [#48532](https://github.com/woocommerce/woocommerce/pull/48532)
+* Dev - E2E tests: fixing flaky logo picker test [#48503](https://github.com/woocommerce/woocommerce/pull/48503)
+* Dev - E2E tests: fixing flaky merchant create variable product test [#48276](https://github.com/woocommerce/woocommerce/pull/48276)
+* Dev - E2E tests: fixing flaky merchant customer list test [#48463](https://github.com/woocommerce/woocommerce/pull/48463)
+* Dev - E2E tests: fixing flaky merchant product attribute test [#48230](https://github.com/woocommerce/woocommerce/pull/48230)
+* Dev - E2E tests: fixing flaky merchant user create and logging [#48446](https://github.com/woocommerce/woocommerce/pull/48446)
+* Dev - E2E tests: fixing flaky shopper checkout coupons [#48555](https://github.com/woocommerce/woocommerce/pull/48555)
+* Dev - E2E tests: fixing flaky shopper search browse products in the shop [#48560](https://github.com/woocommerce/woocommerce/pull/48560)
+* Dev - E2E tests: fixing flaky store owner core profiler test [#48430](https://github.com/woocommerce/woocommerce/pull/48430)
+* Dev - E2E tests: fixing skipped mini cart test [#47756](https://github.com/woocommerce/woocommerce/pull/47756)
+* Dev - E2E tests: fixing skipped tests [#47859](https://github.com/woocommerce/woocommerce/pull/47859)
+* Dev - E2E tests: improve existing merchant e2e tests for creating page and post [#48162](https://github.com/woocommerce/woocommerce/pull/48162)
+* Dev - E2E tests: improve existing util for inserting blocks via shortcut [#48225](https://github.com/woocommerce/woocommerce/pull/48225)
+* Dev - E2E tests: improving cart util and updating relevant tests [#48475](https://github.com/woocommerce/woocommerce/pull/48475)
+* Dev - E2E tests: updated the test ignore pattern for Gutenberg tests project [#47764](https://github.com/woocommerce/woocommerce/pull/47764)
+* Dev - E2E tests: update tests checking if blocks can be added [#48211](https://github.com/woocommerce/woocommerce/pull/48211)
+* Dev - E2E tests: update the report configuration for all core jobs [#48424](https://github.com/woocommerce/woocommerce/pull/48424)
+* Dev - Fix a filters block e2e test that was mistakenly merged incorrectly. [#48122](https://github.com/woocommerce/woocommerce/pull/48122)
+* Dev - Fixing a flaky core profiler e2e test [#47917](https://github.com/woocommerce/woocommerce/pull/47917)
+* Dev - Fix path to test results for api core tests [#48490](https://github.com/woocommerce/woocommerce/pull/48490)
+* Dev - Implement unit test for tracks wcadmin_page_view, wcadmin_tasklist_view, wcadmin_tasklist_task_completed, wcadmin_tasklist_click [#47876](https://github.com/woocommerce/woocommerce/pull/47876)
+* Dev - Include blocks e2e in ci.yml [#48224](https://github.com/woocommerce/woocommerce/pull/48224)
+* Dev - Migrate release smoke workflow to the new CI setup [#48113](https://github.com/woocommerce/woocommerce/pull/48113)
+* Dev - Product Editor: Move variation pricing fields to General tab. [#48155](https://github.com/woocommerce/woocommerce/pull/48155)
+* Dev - Remove the isFeaturePlugin function, which was used to turn off experimental block styling (but was non functional). Also remove associated code in FeatureGating class. [#47866](https://github.com/woocommerce/woocommerce/pull/47866)
+* Dev - Remove WOOCOMMERCE_BLOCKS_PHASE completely from the monorepo, introduce BUNDLE_EXPERIMENTAL_BLOCKS just for the purpose of building/bundling experimental blocks [#47807](https://github.com/woocommerce/woocommerce/pull/47807)
+* Dev - Skipped flaky test: test_order_updated_webhook_delivered_once [#48064](https://github.com/woocommerce/woocommerce/pull/48064)
+* Dev - Streamline the implementation of the Blocks' E2E utilities. [#47660](https://github.com/woocommerce/woocommerce/pull/47660)
+* Dev - Streamline the usage of WP CLI in Blocks E2E tests. [#47869](https://github.com/woocommerce/woocommerce/pull/47869)
+* Dev - Tweak the paths that should trigger e2e tests. [#48067](https://github.com/woocommerce/woocommerce/pull/48067)
+* Dev - Unskip some tests that have been skipped for flakiness [#47772](https://github.com/woocommerce/woocommerce/pull/47772)
+* Dev - Update @wordpress/env version to 9.7.0 [#48443](https://github.com/woocommerce/woocommerce/pull/48443)
+* Dev - Updated Core Profilers XState version to V5 [#48135](https://github.com/woocommerce/woocommerce/pull/48135)
+* Dev - Update Playwright from 1.41.1 to 1.44.1 (latest) and fixed tests [#48291](https://github.com/woocommerce/woocommerce/pull/48291)
+* Dev - Update pnpm-lock with updated React [#47973](https://github.com/woocommerce/woocommerce/pull/47973)
+* Dev - Update the React version in the pnpm-lock file [#47993](https://github.com/woocommerce/woocommerce/pull/47993)
+* Dev - Update the URLs for order-related e2e tests to use new URLs from HPOS [#46397](https://github.com/woocommerce/woocommerce/pull/46397)
+* Dev - [e2e tests] Fix e2e test reports paths [#48320](https://github.com/woocommerce/woocommerce/pull/48320)
+* Tweak - Update Printful label [#48778](https://github.com/woocommerce/woocommerce/pull/48778)
+* Tweak - Add a close button to dismiss store alerts [#48453](https://github.com/woocommerce/woocommerce/pull/48453)
+* Tweak - Adds a defensive check to reduce error log noise when regenerating images. [#47785](https://github.com/woocommerce/woocommerce/pull/47785)
+* Tweak - Adds best practice advice to the API key generation screen. [#48483](https://github.com/woocommerce/woocommerce/pull/48483)
+* Tweak - CYS - Update the copy for the intro tour. [#48202](https://github.com/woocommerce/woocommerce/pull/48202)
+* Tweak - CYS: Refactor routing approach. [#48312](https://github.com/woocommerce/woocommerce/pull/48312)
+* Tweak - Include 'original_post_status' in HPOS edit form. [#48196](https://github.com/woocommerce/woocommerce/pull/48196)
+* Tweak - Minor improvements to BlockTemplatesController instantiation [#48107](https://github.com/woocommerce/woocommerce/pull/48107)
+* Tweak - Only load 'productCount' and 'experimentalBlocksEnabled' settings in admin [#48152](https://github.com/woocommerce/woocommerce/pull/48152)
+* Tweak - Product Editor: Skip momentarily the 'can create a variation option and publish the product' E2E test [#47618](https://github.com/woocommerce/woocommerce/pull/47618)
+* Tweak - Remove checkstyle.xml file [#47844](https://github.com/woocommerce/woocommerce/pull/47844)
+* Tweak - Remove unused woocommerce_task_list_prompt_shown option [#48304](https://github.com/woocommerce/woocommerce/pull/48304)
+* Tweak - Update coming soon banner text to use translation function [#47742](https://github.com/woocommerce/woocommerce/pull/47742)
+* Tweak - Update LYS survey completion track props [#47985](https://github.com/woocommerce/woocommerce/pull/47985)
+* Tweak - Update printful copy. [#48626](https://github.com/woocommerce/woocommerce/pull/48626)
+* Tweak - Update WC blocks e2e tests to WordPress 6.6 [#48436](https://github.com/woocommerce/woocommerce/pull/48436)
+* Tweak - Verify if the coming soon cache is displayed when launching the store and alerts the user if it is still present. [#48586](https://github.com/woocommerce/woocommerce/pull/48586)
+* Performance - Add DISTINCT keyword for smaller response and performance. [#48139](https://github.com/woocommerce/woocommerce/pull/48139)
+* Performance - CYS - Optimize the `Choose a professionally designed theme` intro page image. [#48566](https://github.com/woocommerce/woocommerce/pull/48566)
+* Performance - Replaced `classnames` package with the faster and smaller `clsx` package. [#47760](https://github.com/woocommerce/woocommerce/pull/47760)
+* Performance - Revert changing the title of the edit comments screen when editing a review. [#48485](https://github.com/woocommerce/woocommerce/pull/48485)
+* Enhancement - Accessibility enhancement for the whole shop accounts section [#47144](https://github.com/woocommerce/woocommerce/pull/47144)
+* Enhancement - Add information about block/shortcode/template usage on Cart and Checkout pages to the WC system report. [#48300](https://github.com/woocommerce/woocommerce/pull/48300)
+* Enhancement - CYS: add shuffle feature. [#47356](https://github.com/woocommerce/woocommerce/pull/47356)
+* Enhancement - CYS: allow to the user to move the pattern. [#47322](https://github.com/woocommerce/woocommerce/pull/47322)
+* Enhancement - Enhancement editor loading speed [#47425](https://github.com/woocommerce/woocommerce/pull/47425)
+* Enhancement - Handle core profiler get countries error [#48317](https://github.com/woocommerce/woocommerce/pull/48317)
+* Enhancement - If a variable product doesn't have a Product Image but variations do have images, the zoom and flex slider will be initiated as expected [#47714](https://github.com/woocommerce/woocommerce/pull/47714)
+* Enhancement - Improve spacing between steps in the Checkout block on mobile and desktop [#47565](https://github.com/woocommerce/woocommerce/pull/47565)
+* Enhancement - Increase connection timeout to 30 seconds for the requests in WCCOM connection flow [#47842](https://github.com/woocommerce/woocommerce/pull/47842)
+* Enhancement - Limit coming soon options API call to home screen [#48303](https://github.com/woocommerce/woocommerce/pull/48303)
+* Enhancement - Modified order status tooltip labels [#47861](https://github.com/woocommerce/woocommerce/pull/47861)
+* Enhancement - Optimize text wrapping for wc admin pages [#48131](https://github.com/woocommerce/woocommerce/pull/48131)
+* Enhancement - Remove the previous product management experience [#47814](https://github.com/woocommerce/woocommerce/pull/47814)
+
= 9.0.2 2024-06-24 =
**WooCommerce**
@@ -21,9 +271,9 @@
* Fix - Fix settings-api textarea validation to prevent insertion of iframes in description areas by default [#48432](https://github.com/woocommerce/woocommerce/pull/48432)
* Fix - #47626 changed the classes on the legacy admin settings save button and broke saving standard tax rates [#48201](https://github.com/woocommerce/woocommerce/pull/48201)
* Fix - Revert "Remove customer-effort-score-tracks" feature flag #48235 [#48235](https://github.com/woocommerce/woocommerce/pull/48235)
-* Fix - Fix db update notice redirection bug where it redirects without checking for db update action. Changelog Entry Comment [#48163](https://github.com/woocommerce/woocommerce/pull/48163)
+* Fix - Fix db update notice redirection bug where it redirects without checking for db update action. [#48163](https://github.com/woocommerce/woocommerce/pull/48163)
* Fix - Add missing URL to discover more link in LYS tour [#48109](https://github.com/woocommerce/woocommerce/pull/48109)
-* Fix - Fix: "On Sale" collection isn't displaying on Editor side [#47994](https://github.com/woocommerce/woocommerce/pull/47994)
+* Fix - Fix: "On Sale" collection isn't displaying on Editor side [#47994](https://github.com/woocommerce/woocommerce/pull/47994)
* Fix - Make the plugin autoinstall process more robust [#47798](https://github.com/woocommerce/woocommerce/pull/47798)
* Fix - Prevent tracking files from being enqueued on the front end. [#47938](https://github.com/woocommerce/woocommerce/pull/47938)
* Fix - Fix: Product Collection block does not display properly when editing template/post [#47871](https://github.com/woocommerce/woocommerce/pull/47871)
diff --git a/docs/code-snippets/check_payment_method_support.md b/docs/code-snippets/check_payment_method_support.md
new file mode 100644
index 00000000000..d28fcef8b9a
--- /dev/null
+++ b/docs/code-snippets/check_payment_method_support.md
@@ -0,0 +1,44 @@
+---
+post_title: Check if a Payment Method Support Refunds, Subscriptions or Pre-orders
+menu_title: Payment method support for refunds, subscriptions, pre-orders
+tags: payment-methods
+current wccom url: https://woocommerce.com/document/check-if-payment-gateway-supports-refunds-subscriptions-preorders/
+---
+
+# Check if a Payment Method Support Refunds, Subscriptions or Pre-orders
+
+If a payment method's documentation doesn’t clearly outline the supported features, you can often find what features are supported by looking at payment methods code.
+
+Payment methods can add support for certain features from WooCommerce and its extensions. For example, a payment method can support refunds, subscriptions or pre-orders functionality.
+
+## Simplify Commerce example
+
+Taking the Simplify Commerce payment method as an example, open the plugin files in your favorite editor and search for `$this->supports`. You'll find the supported features:
+
+```php
+class WC_Gateway_Simplify_Commerce extends WC_Payment_Gateway {
+
+/** * Constructor */
+ public function __construct() {
+ $this->id
+ = 'simplify_commerce';
+ $this->method_title
+ = __( 'Simplify Commerce', 'woocommerce' );
+ $this->method_description = __( 'Take payments via Simplify Commerce - uses simplify.js to create card tokens and the Simplify Commerce SDK. Requires SSL when sandbox is disabled.', 'woocommerce' );
+ $this->has_fields = true;
+ $this->supports = array(
+ 'subscriptions',
+ 'products',
+ 'subscription_cancellation',
+ 'subscription_reactivation',
+ 'subscription_suspension',
+ 'subscription_amount_changes',
+ 'subscription_payment_method_change',
+ 'subscription_date_changes',
+ 'default_credit_card_form',
+ 'refunds',
+ 'pre-orders'
+ );
+```
+
+If you don’t find `$this->supports` in the plugin files, that may mean that the payment method isn’t correctly declaring support for refunds, subscripts or pre-orders.
diff --git a/docs/code-snippets/configuring_special_tax_scenarios.md b/docs/code-snippets/configuring_special_tax_scenarios.md
new file mode 100644
index 00000000000..6b3c7e07126
--- /dev/null
+++ b/docs/code-snippets/configuring_special_tax_scenarios.md
@@ -0,0 +1,81 @@
+---
+post_title: Code snippets for configuring special tax scenarios
+menu_title: Configuring special tax scenarios
+tags: code-snippet, tax
+current wccom url: https://woocommerce.com/document/setting-up-taxes-in-woocommerce/configuring-specific-tax-setups-in-woocommerce/#configuring-special-tax-setups
+---
+
+# Code snippets for configuring special tax scenarios
+
+## Scenario A: Charge the same price regardless of location and taxes
+
+Scenario A: Charge the same price regardless of location and taxes
+
+If a store enters product prices including taxes, but levies various location-based tax rates, the prices will appear to change depending on which tax rate is applied. In reality, the base price remains the same, but the taxes influence the total. [Follow this link for a detailed explanation](https://woocommerce.com/document/how-taxes-work-in-woocommerce/#cross-border-taxes).
+
+Some merchants prefer to dynamically change product base prices to account for the changes in taxes and so keep the total price consistent regardless of tax rate. Enable that functionality by adding the following snippet to your child theme’s functions.php file or via a code snippet plugin.
+
+```php
+cart->subtotal <= 110 )
+ $tax_class = 'Zero Rate';
+
+ return $tax_class;
+}
+```
+
+## Scenario C: Apply different tax rates based on the customer role
+
+Some merchants may require different tax rates to be applied based on a customer role to accommodate for wholesale status or tax exemption.
+
+To enable this functionality, add the following snippet to your child theme’s functions.php file or via a code snippet plugin. In this snippet, users with “administrator” capabilities will be assigned the **Zero rate tax class**. Adjust it according to your requirements.
+
+```php
+get_id(), 'woo_custom_field', true );
+
+ if ( ! empty( $custom_field_value ) ) {
+ echo '' . esc_html( $custom_field_value ) . '
';
+ }
+}
+
+add_action( 'woocommerce_before_add_to_cart_form', 'woocommerce_custom_field_example', 10 );
+```
diff --git a/docs/code-snippets/free_shipping_customization.md b/docs/code-snippets/free_shipping_customization.md
new file mode 100644
index 00000000000..6d4d700a39f
--- /dev/null
+++ b/docs/code-snippets/free_shipping_customization.md
@@ -0,0 +1,147 @@
+---
+post_title: Free Shipping Customizations
+menu_title: Free shipping customizations
+tags: code-snippets
+current wccom url: https://woocommerce.com/document/free-shipping/#advanced-settings-customization
+combined with: https://woocommerce.com/document/hide-other-shipping-methods-when-free-shipping-is-available/#use-a-plugin
+---
+
+## Free Shipping: Advanced Settings/Customization
+
+### Overview
+
+By default, WooCommerce shows all shipping methods that match the customer and the cart contents. This means Free Shipping also shows along with Flat Rate and other Shipping Methods.
+
+The functionality to hide all other methods, and only show Free Shipping, requires either custom PHP code or a plugin/extension.
+
+### Adding code
+
+Before adding snippets, clear your WooCommerce cache. Go to WooCommerce > System Status > Tools > WooCommerce Transients > Clear transients.
+
+Add this code to your child theme’s `functions.php`, or via a plugin that allows custom functions to be added. Please don’t add custom code directly to a parent theme’s `functions.php` as changes are entirely erased when a parent theme updates.
+
+## Code Snippets
+
+### Enabling or Disabling Free Shipping via Hooks
+
+You can hook into the `is_available` function of the free shipping method.
+
+```php
+return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available );
+```
+
+This means you can use `add_filter()` on `woocommerce_shipping_free_shipping_is_available` and return `true` or `false`.
+
+### How do I only show Free Shipping?
+
+The following snippet hides everything but `free_shipping`, if it’s available and the customer's cart qualifies.
+
+```php
+/**
+ * Hide shipping rates when free shipping is available.
+ * Updated to support WooCommerce 2.6 Shipping Zones.
+ *
+ * @param array $rates Array of rates found for the package.
+ * @return array
+ */
+function my_hide_shipping_when_free_is_available( $rates ) {
+ $free = array();
+ foreach ( $rates as $rate_id => $rate ) {
+ if ( 'free_shipping' === $rate->method_id ) {
+ $free[ $rate_id ] = $rate;
+ break;
+ }
+ }
+ return ! empty( $free ) ? $free : $rates;
+}
+add_filter( 'woocommerce_package_rates', 'my_hide_shipping_when_free_is_available', 100 );
+```
+
+### How do I only show Local Pickup and Free Shipping?
+
+The snippet below hides everything but `free_shipping` and `local_pickup`, if it’s available and the customer's cart qualifies.
+
+```php
+
+/**
+ * Hide shipping rates when free shipping is available, but keep "Local pickup"
+ * Updated to support WooCommerce 2.6 Shipping Zones
+ */
+
+function hide_shipping_when_free_is_available( $rates, $package ) {
+ $new_rates = array();
+ foreach ( $rates as $rate_id => $rate ) {
+ // Only modify rates if free_shipping is present.
+ if ( 'free_shipping' === $rate->method_id ) {
+ $new_rates[ $rate_id ] = $rate;
+ break;
+ }
+ }
+
+ if ( ! empty( $new_rates ) ) {
+ //Save local pickup if it's present.
+ foreach ( $rates as $rate_id => $rate ) {
+ if ('local_pickup' === $rate->method_id ) {
+ $new_rates[ $rate_id ] = $rate;
+ break;
+ }
+ }
+ return $new_rates;
+ }
+
+ return $rates;
+}
+
+add_filter( 'woocommerce_package_rates', 'hide_shipping_when_free_is_available', 10, 2 );
+```
+
+### Only show free shipping in all states except…
+
+This snippet results in showing only free shipping in all states except the exclusion list. It hides free shipping if the customer is in one of the states listed:
+
+```php
+/**
+ * Hide ALL shipping options when free shipping is available and customer is NOT in certain states
+ *
+ * Change $excluded_states = array( 'AK','HI','GU','PR' ); to include all the states that DO NOT have free shipping
+ */
+add_filter( 'woocommerce_package_rates', 'hide_all_shipping_when_free_is_available' , 10, 2 );
+
+/**
+ * Hide ALL Shipping option when free shipping is available
+ *
+ * @param array $available_methods
+ */
+function hide_all_shipping_when_free_is_available( $rates, $package ) {
+
+ $excluded_states = array( 'AK','HI','GU','PR' );
+ if( isset( $rates['free_shipping'] ) AND !in_array( WC()->customer->shipping_state, $excluded_states ) ) :
+ // Get Free Shipping array into a new array
+ $freeshipping = array();
+ $freeshipping = $rates['free_shipping'];
+
+ // Empty the $available_methods array
+ unset( $rates );
+
+ // Add Free Shipping back into $avaialble_methods
+ $rates = array();
+ $rates[] = $freeshipping;
+
+ endif;
+
+ if( isset( $rates['free_shipping'] ) AND in_array( WC()->customer->shipping_state, $excluded_states ) ) {
+
+ // remove free shipping option
+ unset( $rates['free_shipping'] );
+
+ }
+
+ return $rates;
+}
+```
+
+### Enable Shipping Methods on a per Class / Product Basis, split orders, or other scenarios?
+
+Need more flexibility? Take a look at our [premium Shipping Method extensions](https://woocommerce.com/product-category/woocommerce-extensions/shipping-methods/).
+
+
diff --git a/docs/code-snippets/legacy_local_pickup_advacned_settings_and_customization.md b/docs/code-snippets/legacy_local_pickup_advacned_settings_and_customization.md
new file mode 100644
index 00000000000..d05a8c7e788
--- /dev/null
+++ b/docs/code-snippets/legacy_local_pickup_advacned_settings_and_customization.md
@@ -0,0 +1,37 @@
+---
+post_title: Legacy Local Pickup Advanced Settings and Customization
+tags: code-snippet
+current wccom url: https://woocommerce.com/document/local-pickup/#advanced-settings-customization
+note: Docs links out to Skyverge's site for howto add a custom email - do we have our own alternative?
+---
+
+# Advanced settings and customization for legacy Local Pickup
+
+## Disable local taxes when using local pickup
+
+Local Pickup calculates taxes based on your store’s location (address) by default, and not the customer’s address. Add this snippet at the end of your theme's `functions.php` to use your standard tax configuration instead:
+
+```php
+add_filter( 'woocommerce_apply_base_tax_for_local_pickup', '__return_false' );
+```
+
+Regular taxes is then used when local pickup is selected, instead of store-location-based taxes.
+
+## Changing the location for local taxes
+
+To charge local taxes based on the postcode and city of the local pickup location, you need to define the shop’s base city and post code using this example code:
+
+```php
+add_filter( 'woocommerce_countries_base_postcode', create_function( '', 'return "80903";' ) );
+add_filter( 'woocommerce_countries_base_city', create_function( '', 'return "COLORADO SPRINGS";' ) );
+```
+
+Update `80903` to reflect your preferred postcode/zip, and `COLORADO SPRINGS` with your preferred town or city.
+
+## Custom emails for local pickup
+
+_Shipping Address_ is not displayed on the admin order emails when Local Pickup is used as the shipping method.
+
+Since all core shipping options use the standard order flow, customers receive the same order confirmation email whether they select local pickup or any other shipping option.
+Use this guide to create custom emails for local pickup if you’d like to send a separate email for local pickup orders: [How to Add a Custom WooCommerce Email](https://www.skyverge.com/blog/how-to-add-a-custom-woocommerce-email/).
+
diff --git a/docs/code-snippets/making_translations_upgrade_safe.md b/docs/code-snippets/making_translations_upgrade_safe.md
new file mode 100644
index 00000000000..2753c1f43bf
--- /dev/null
+++ b/docs/code-snippets/making_translations_upgrade_safe.md
@@ -0,0 +1,29 @@
+---
+post_title: Making your translation upgrade safe
+menu_title: Translation upgrade safety
+tags: code-snippet
+current wccom url: https://woocommerce.com/document/woocommerce-localization/#making-your-translation-upgrade-safe
+---
+
+# Making your translation upgrade safe
+
+Like all other plugins, WooCommerce keeps translations in `wp-content/languages/plugins`.
+
+However, if you want to include a custom translation, you can add them to `wp-content/languages/woocommerce`, or you can use a snippet to load a custom translation stored elsewhere:
+
+```php
+// Code to be placed in functions.php of your theme or a custom plugin file.
+add_filter( 'load_textdomain_mofile', 'load_custom_plugin_translation_file', 10, 2 );
+
+/*
+ * Replace 'textdomain' with your plugin's textdomain. e.g. 'woocommerce'.
+ * File to be named, for example, yourtranslationfile-en_GB.mo
+ * File to be placed, for example, wp-content/lanaguages/textdomain/yourtranslationfile-en_GB.mo
+ */
+function load_custom_plugin_translation_file( $mofile, $domain ) {
+ if ( 'textdomain' === $domain ) {
+ $mofile = WP_LANG_DIR . '/textdomain/yourtranslationfile-' . get_locale() . '.mo';
+ }
+ return $mofile;
+}
+```
diff --git a/docs/code-snippets/shipping_method_api.md b/docs/code-snippets/shipping_method_api.md
new file mode 100644
index 00000000000..5752e45f2ce
--- /dev/null
+++ b/docs/code-snippets/shipping_method_api.md
@@ -0,0 +1,210 @@
+---
+post_title: Shipping Method API
+menu_title: Shipping method API
+tags: shipping, API
+current wccom url: https://woocommerce.com/document/shipping-method-api/
+---
+
+
+# Shipping Method API
+
+WooCommerce has a shipping method API which plugins can use to add their own rates. This article outlines steps to create a new shipping method and interact with the API.
+
+## Create a plugin
+
+First, create a regular WordPress/WooCommerce plugin (see [Create a plugin](https://woocommerce.com/document/create-a-plugin/)). You’ll define your shipping method class in this plugin file and maintain it outside of WooCommerce.
+
+## Create a function to house your class
+
+To ensure the classes you need to extend exist, you should wrap your class in a function which is called after all plugins are loaded:
+
+```php
+function your_shipping_method_init() {
+ // Your class will go here
+}
+
+add_action( 'woocommerce_shipping_init', 'your_shipping_method_init' );
+```
+
+## Create your class
+
+Create your class and place it inside the function you just created. Make sure it extends the shipping method class so that you have access to the API. You’ll see below we also init our shipping method options.
+
+```php
+if ( ! class_exists( 'WC_Your_Shipping_Method' ) ) {
+ class WC_Your_Shipping_Method extends WC_Shipping_Method {
+ /**
+ * Constructor for your shipping class
+ *
+ * @access public
+ * @return void
+ */
+ public function __construct() {
+ $this->id = 'your_shipping_method';
+ $this->title = __( 'Your Shipping Method' );
+ $this->method_description = __( 'Description of your shipping method' ); //
+ $this->enabled = "yes"; // This can be added as an setting but for this example its forced enabled
+ $this->init();
+ }
+
+ /**
+ * Init your settings
+ *
+ * @access public
+ * @return void
+ */
+ function init() {
+ // Load the settings API
+ $this->init_form_fields(); // This is part of the settings API. Override the method to add your own settings
+ $this->init_settings(); // This is part of the settings API. Loads settings you previously init.
+
+ // Save settings in admin if you have any defined
+ add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
+ }
+
+ /**
+ * calculate_shipping function.
+ *
+ * @access public
+ * @param mixed $package
+ * @return void
+ */
+ public function calculate_shipping( $package ) {
+ // This is where you'll add your rates
+ }
+ }
+}
+```
+
+As well as declaring your class, you also need to tell WooCommerce it exists with another function:
+
+```php
+function add_your_shipping_method( $methods ) {
+ $methods['your_shipping_method'] = 'WC_Your_Shipping_Method';
+ return $methods;
+}
+
+add_filter( 'woocommerce_shipping_methods', 'add_your_shipping_method' );
+```
+
+## Defining settings/options
+
+You can define your options once the above is in place by using the settings API. In the snippets above you’ll notice we `init_form_fields` and `init_settings`. These load up the settings API. To see how to add settings, see [WooCommerce settings API](https://woocommerce.com/document/settings-api/).
+
+## The calculate_shipping() method
+
+Add your rates by usign the `calculate_shipping()` method. WooCommerce calls this when doing shipping calculations. Do your plugin specific calculations here and then add the rates via the API. Like so:
+
+```php
+$rate = array(
+ 'label' => "Label for the rate",
+ 'cost' => '10.99',
+ 'calc_tax' => 'per_item'
+);
+
+// Register the rate
+$this->add_rate( $rate );
+```
+
+`Add_rate` takes an array of options. The defaults/possible values for the array are as follows:
+
+```php
+$defaults = array(
+ 'label' => '', // Label for the rate
+ 'cost' => '0', // Amount for shipping or an array of costs (for per item shipping)
+ 'taxes' => '', // Pass an array of taxes, or pass nothing to have it calculated for you, or pass 'false' to calculate no tax for this method
+ 'calc_tax' => 'per_order' // Calc tax per_order or per_item. Per item needs an array of costs passed via 'cost'
+);
+```
+
+Your shipping method can pass as many rates as you want – just ensure that the id for each is different. The user will get to choose rate during checkout.
+
+
+## Piecing it all together
+
+The skeleton shipping method code all put together looks like this:
+
+```php
+id = 'your_shipping_method'; // Id for your shipping method. Should be uunique.
+ $this->method_title = __( 'Your Shipping Method' ); // Title shown in admin
+ $this->method_description = __( 'Description of your shipping method' ); // Description shown in admin
+
+ $this->enabled = "yes"; // This can be added as an setting but for this example its forced enabled
+ $this->title = "My Shipping Method"; // This can be added as an setting but for this example its forced.
+
+ $this->init();
+ }
+
+ /**
+ * Init your settings
+ *
+ * @access public
+ * @return void
+ */
+ function init() {
+ // Load the settings API
+ $this->init_form_fields(); // This is part of the settings API. Override the method to add your own settings
+ $this->init_settings(); // This is part of the settings API. Loads settings you previously init.
+
+ // Save settings in admin if you have any defined
+ add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
+ }
+
+ /**
+ * calculate_shipping function.
+ *
+ * @access public
+ * @param array $package
+ * @return void
+ */
+ public function calculate_shipping( $package = array() ) {
+ $rate = array(
+ 'label' => $this->title,
+ 'cost' => '10.99',
+ 'calc_tax' => 'per_item'
+ );
+
+ // Register the rate
+ $this->add_rate( $rate );
+ }
+ }
+ }
+ }
+
+ add_action( 'woocommerce_shipping_init', 'your_shipping_method_init' );
+
+ function add_your_shipping_method( $methods ) {
+ $methods['your_shipping_method'] = 'WC_Your_Shipping_Method';
+ return $methods;
+ }
+
+ add_filter( 'woocommerce_shipping_methods', 'add_your_shipping_method' );
+}
+```
+
+For further information, please check out the [Shipping Method API Wiki](https://github.com/woocommerce/woocommerce/wiki/Shipping-Method-API).
diff --git a/docs/code-snippets/ssl_and_https_and_woocommerce_websites_behind_load_balanacers_or_reverse_proxies.md b/docs/code-snippets/ssl_and_https_and_woocommerce_websites_behind_load_balanacers_or_reverse_proxies.md
new file mode 100644
index 00000000000..8f6d733bec3
--- /dev/null
+++ b/docs/code-snippets/ssl_and_https_and_woocommerce_websites_behind_load_balanacers_or_reverse_proxies.md
@@ -0,0 +1,22 @@
+---
+post_title: SSL and HTTPS and WooCommerce
+menu_title: SSL and HTTPS and WooCommerce
+tags: code-snippet
+current wccom url: https://woocommerce.com/document/ssl-and-https/#websites-behind-load-balancers-or-reverse-proxies
+---
+
+## Websites behind load balancers or reverse proxies
+
+WooCommerce uses the `is_ssl()` WordPress function to verify if your website using SSL or not.
+
+`is_ssl()` checks if the connection is via HTTPS or on Port 443. However, this won’t work for websites behind load balancers, especially websites hosted at Network Solutions. For details, read [WordPress is_ssl() function reference notes](https://codex.wordpress.org/Function_Reference/is_ssl#Notes).
+
+Websites behind load balancers or reverse proxies that support `HTTP_X_FORWARDED_PROTO` can be fixed by adding the following code to the `wp-config.php` file, above the require_once call:
+
+```php
+if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' == $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
+ $_SERVER['HTTPS'] = 'on';
+}
+```
+
+**Note:** If you use CloudFlare, you need to configure it. Check their documentation.
diff --git a/docs/code-snippets/uninstall_remove_all_woocommerce_data.md b/docs/code-snippets/uninstall_remove_all_woocommerce_data.md
new file mode 100644
index 00000000000..be83e2bca03
--- /dev/null
+++ b/docs/code-snippets/uninstall_remove_all_woocommerce_data.md
@@ -0,0 +1,26 @@
+---
+post_title: Uninstall and remove all WooCommerce Data
+menu_title: Uninstalling and removing data
+tags: code-snippet
+current wccom url: https://woocommerce.com/document/installing-uninstalling-woocommerce/#uninstalling-woocommerce
+---
+
+# Uninstall and remove all WooCommerce Data
+
+The WooCommerce plugin can be uninstalled like any other WordPress plugin. By default, the WooCommerce data is left in place though.
+
+If you need to remove *all* WooCommerce data as well, including products, order data, coupons, etc., you need to to modify the site’s `wp-config.php` *before* deactivating and deleting the WooCommerce plugin.
+
+As this action is destructive and permanent, the information is provided as is. WooCommerce Support cannot help with this process or anything that happens as a result.
+
+To fully remove all WooCommerce data from your WordPress site, open `wp-config.php`, scroll down to the bottom of the file, and add the following constant on its own line above `/* That’s all, stop editing. */`.
+
+```php
+define( 'WC_REMOVE_ALL_DATA', true );
+
+/* That’s all, stop editing! Happy publishing. */
+```
+
+Then, once the changes are saved to the file, when you deactivate and delete WooCommerce, all of its data is removed from your WordPress site database.
+
+![Uninstall WooCommerce WPConfig](https://woocommerce.com/wp-content/uploads/2020/03/uninstall_wocommerce_plugin_wpconfig.png)
diff --git a/docs/code-snippets/using_nginx_server_to_protect_your_uploads_directory.md b/docs/code-snippets/using_nginx_server_to_protect_your_uploads_directory.md
new file mode 100644
index 00000000000..3f3171a90e5
--- /dev/null
+++ b/docs/code-snippets/using_nginx_server_to_protect_your_uploads_directory.md
@@ -0,0 +1,35 @@
+---
+post_title: Using NGINX server to protect your upload directory
+menu_title: NGINX server to protect upload directory
+tags: code-snippet
+current wccom url: https://woocommerce.com/document/digital-downloadable-product-handling/#protecting-your-uploads-directory
+---
+
+## Using NGINX server to protect your upload directory
+
+If you using NGINX server for your site along with **X-Accel-Redirect/X-Sendfile** or **Force Downloads** download method, it is necessary that you add this configuration for better security:
+
+```php
+# Protect WooCommerce upload folder from being accessed directly.
+# You may want to change this config if you are using "X-Accel-Redirect/X-Sendfile" or "Force Downloads" method for downloadable products.
+# Place this config towards the end of "server" block in NGINX configuration.
+location ~* /wp-content/uploads/woocommerce_uploads/ {
+ if ( $upstream_http_x_accel_redirect = "" ) {
+ return 403;
+ }
+ internal;
+}
+```
+
+And this the configuration in case you are using **Redirect only** download method:
+
+```php
+# Protect WooCommerce upload folder from being accessed directly.
+# You may want to change this config if you are using "Redirect Only" method for downloadable products.
+# Place this config towards the end of "server" block in NGINX configuration.
+location ~* /wp-content/uploads/woocommerce_uploads/ {
+ autoindex off;
+}
+```
+
+If you do not know which web server you are using, please reach out to your host along with a link to this support page.
diff --git a/docs/docs-manifest.json b/docs/docs-manifest.json
index 6d6f972c602..96457b28a0b 100644
--- a/docs/docs-manifest.json
+++ b/docs/docs-manifest.json
@@ -52,6 +52,15 @@
"category_slug": "code-snippets",
"category_title": "Code Snippets",
"posts": [
+ {
+ "post_title": "Using NGINX server to protect your upload directory",
+ "menu_title": "NGINX server to protect upload directory",
+ "tags": "code-snippet",
+ "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/using_nginx_server_to_protect_your_uploads_directory.md",
+ "hash": "5d7afe5c8217c3a5f753eb2f468b8304f7f9b5b1275461abf2146e4de82ed6b2",
+ "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/using_nginx_server_to_protect_your_uploads_directory.md",
+ "id": "8b325d3483f9a8d09961ca1082839752137faebf"
+ },
{
"post_title": "Useful core functions",
"tags": "code-snippet",
@@ -60,6 +69,15 @@
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/useful-functions.md",
"id": "0d99f1dee7c104b5899fd62b96157fb6709ebfb8"
},
+ {
+ "post_title": "Uninstall and remove all WooCommerce Data",
+ "menu_title": "Uninstalling and removing data",
+ "tags": "code-snippet",
+ "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/uninstall_remove_all_woocommerce_data.md",
+ "hash": "73483ff158ceac81685a9cd52335dc98e99ac7f84d89cdbcf4ce994e18afe30d",
+ "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/uninstall_remove_all_woocommerce_data.md",
+ "id": "36b571fcf2471737729ab4769e2c721b2248187f"
+ },
{
"post_title": "Unhook and remove WooCommerce emails",
"tags": "code-snippet",
@@ -68,6 +86,24 @@
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/unhook--remove-woocommerce-emails.md",
"id": "0fdfe3b483ae74a9e5dc1fc21b80814462222ec3"
},
+ {
+ "post_title": "SSL and HTTPS and WooCommerce",
+ "menu_title": "SSL and HTTPS and WooCommerce",
+ "tags": "code-snippet",
+ "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/ssl_and_https_and_woocommerce_websites_behind_load_balanacers_or_reverse_proxies.md",
+ "hash": "92a5091c27d1af6c0b49df143dd13886fb2cb30538fa877f68000cab69f4f502",
+ "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/ssl_and_https_and_woocommerce_websites_behind_load_balanacers_or_reverse_proxies.md",
+ "id": "78d5b5a20ce6471b74f809386eff41fffe2d1adb"
+ },
+ {
+ "post_title": "Shipping Method API",
+ "menu_title": "Shipping method API",
+ "tags": "shipping, API",
+ "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/shipping_method_api.md",
+ "hash": "bd7cbc361fe94acaa40fbc5befa8d14f302705a9d700dd7d7e78a482b003fe0b",
+ "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/shipping_method_api.md",
+ "id": "a419b97e5594918a015c61227ad9226c509eb314"
+ },
{
"post_title": "Rename a country",
"tags": "code-snippet",
@@ -84,6 +120,15 @@
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/number-of-products-per-row.md",
"id": "7369dc328c49206771a2f8d0da5d920c480b5207"
},
+ {
+ "post_title": "Making your translation upgrade safe",
+ "menu_title": "Translation upgrade safety",
+ "tags": "code-snippet",
+ "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/making_translations_upgrade_safe.md",
+ "hash": "e2d296630d7af888a072de51870f3b4ff311b3c29f706fda735bd8f9122c8710",
+ "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/making_translations_upgrade_safe.md",
+ "id": "0c1add87ef9f5452b4c8404bb55021ad8265c171"
+ },
{
"post_title": "Add link to logged data",
"menu_title": "Add link to logged data",
@@ -93,6 +138,40 @@
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/link-to-logged-data.md",
"id": "34da337f79be5ce857024f541a99d302174ca37d"
},
+ {
+ "post_title": "Legacy Local Pickup Advanced Settings and Customization",
+ "tags": "code-snippet",
+ "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/legacy_local_pickup_advacned_settings_and_customization.md",
+ "hash": "d0269f1ee2700356672a032e4e54491666b901765045f7c5224ef07eeb9d9598",
+ "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/legacy_local_pickup_advacned_settings_and_customization.md",
+ "id": "c4d4a2276fc251082a80a8330eea1eb62a97c3bb"
+ },
+ {
+ "post_title": "Free Shipping Customizations",
+ "menu_title": "Free shipping customizations",
+ "tags": "code-snippets",
+ "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/free_shipping_customization.md",
+ "hash": "c18884a45e4e1cc7b174820c2553d2722df95b98f6783c2700096a5b7e19bffd",
+ "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/free_shipping_customization.md",
+ "id": "cac6f1ccd661588e9a5fa7405643e9c6d4da388e"
+ },
+ {
+ "post_title": "Displaying Custom Fields in Your Theme or Site",
+ "menu_title": "Displaying custom fields in theme",
+ "tags": "code-snippet",
+ "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/displaying_custom_fields_in_your_theme_or_site.md",
+ "hash": "8048c2e9e5d25268d17d4f4ca7929e265eddbd4653318dd8f544856ddecd39dd",
+ "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/displaying_custom_fields_in_your_theme_or_site.md",
+ "id": "3e3fd004afda355cf9dbb05f0967523d6d0da1ce"
+ },
+ {
+ "post_title": "Disabling Marketplace Suggestions Programmatically",
+ "menu_title": "Disabling marketplace suggestions",
+ "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/disabling_marketplace_suggestions_programmatically.md",
+ "hash": "3d5bd50d64a46efaea99efb0a87dfdb8882cb83598b7be8a8154ad0e464eb6f5",
+ "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/disabling_marketplace_suggestions_programmatically.md",
+ "id": "94a7a28e5dd3d9394650e66abec2429445e87028"
+ },
{
"post_title": "Customizing checkout fields using actions and filters",
"tags": "code-snippet",
@@ -101,6 +180,24 @@
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/customising-checkout-fields.md",
"id": "83097d3b7414557fc80dcf9f8f1a708bbdcdd884"
},
+ {
+ "post_title": "Code snippets for configuring special tax scenarios",
+ "menu_title": "Configuring special tax scenarios",
+ "tags": "code-snippet, tax",
+ "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/configuring_special_tax_scenarios.md",
+ "hash": "128193e0e980f484f354c93e59d34c3948f112e4a1c99158cf3e5d9969db9352",
+ "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/configuring_special_tax_scenarios.md",
+ "id": "a8ab8b6734ba2ac5af7c6653635d15548abdab2a"
+ },
+ {
+ "post_title": "Check if a Payment Method Support Refunds, Subscriptions or Pre-orders",
+ "menu_title": "Payment method support for refunds, subscriptions, pre-orders",
+ "tags": "payment-methods",
+ "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/code-snippets/check_payment_method_support.md",
+ "hash": "6cae4b1fda5980c327c99d6bae8b1978fd05849f07179f0699a174b57d27b862",
+ "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/code-snippets/check_payment_method_support.md",
+ "id": "2919c9fc523bce46f43a5f35f821d0c6623c5ede"
+ },
{
"post_title": "Change a currency symbol",
"tags": "code-snippet",
@@ -350,7 +447,7 @@
{
"post_title": "Logging in WooCommerce",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/extension-development/logging.md",
- "hash": "844689b6d9c482fb217a512db6ddab0afd4d76b2b9e378a9302681d2a8dfe517",
+ "hash": "7e66b9ea605944c5926cf6099fb8fb323976c014fef7dd768c91cef17b091edd",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/extension-development/logging.md",
"id": "c684e2efba45051a4e1f98eb5e6ef6bab194f25c"
},
@@ -716,7 +813,7 @@
"post_title": "Product editor development handbook",
"menu_title": "Development handbook",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/docs/product-editor-development/product-editor.md",
- "hash": "cc5f82b66e949e3df2928b5e6b1217e8804c43b8e7b75ebc930cd0f90aef7bbe",
+ "hash": "b574a4a5476899342cd229033a22ecdf9859914ea34446f8276e2b0ad5cb8c7f",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/docs/product-editor-development/product-editor.md",
"id": "59450404de2750d918137e7cf523e52bedfd7214",
"links": {
@@ -1367,5 +1464,5 @@
"categories": []
}
],
- "hash": "cf89b5c07e5b7a9007eb4afe021b02ddce4611c81a863b0a431ae57491a3b37f"
+ "hash": "d8fe058ebcce4d1f40585f1634b1e98e27063d81dd0dd7783217ab60d0bc915a"
}
\ No newline at end of file
diff --git a/docs/extension-development/logging.md b/docs/extension-development/logging.md
index a1e7298161b..dcedb9dc5e1 100644
--- a/docs/extension-development/logging.md
+++ b/docs/extension-development/logging.md
@@ -140,9 +140,9 @@ wc_get_logger()->info(
### Best practices
-* Rather than using the `WC_Logger`‘s `log()` method directly, it's better to use one of the wrapper methods that's specific to the log level. E.g. `info()` or `error()`.
+* Rather than using the `WC_Logger`'s `log()` method directly, it's better to use one of the wrapper methods that's specific to the log level. E.g. `info()` or `error()`.
* Write a message that is a complete, coherent sentence. This will make it more useful for people who aren't familiar with the codebase.
-* Log messages should not be translatable (see the discussion about this in the comments). Keeping the message in English makes it easier to search for solutions based on the message contents, and also makes it easier for Happiness Engineers to understand what's happening, since they may not speak the same language as the site owner.
+* Log messages should not be translatable. Keeping the message in English makes it easier to search for solutions based on the message contents, and also makes it easier for anyone troubleshooting to understand what's happening, since they may not speak the same language as the site owner.
* Ideally, each log entry message should be a single line (i.e. no line breaks within the message string). Additional lines or extra data should be put in the context array.
* Avoid outputting structured data in the message string. Put it in a key in the context array instead. The logger will handle converting it to JSON and making it legible in the log viewer.
* If you need to include a stack trace, let the logger generate it for you.
@@ -159,7 +159,7 @@ The `WC_Logger` class can be substituted for another class via the `woocommerce_
In WooCommerce, a log handler is a PHP class that takes the raw log data and transforms it into a log entry that can be stored or dispatched. WooCommerce ships with four different log handler classes:
-* `Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2`: This is the default handler, representing the "file system" log storage method. It records log entries to files.
+* `Automattic\\WooCommerce\\Internal\\Admin\\Logging\\LogHandlerFileV2`: This is the default handler, representing the "file system" log storage method. It records log entries to files.
* `WC_Log_Handler_File`: This is the old default handler that also records log entries to files. It may be deprecated in the future, and it is not recommended to use this class or extend it.
* `WC_Log_Handler_DB`: This handler represents the "database" log storage method. It records log entries to the database.
* `WC_Log_Handler_Email`: This handler does not store log entries, but instead sends them as email messages. Emails are sent to the site admin email address. This handler has [some limitations](https://github.com/woocommerce/woocommerce/blob/fe81a4cf27601473ad5c394a4f0124c785aaa4e6/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-email.php#L15-L27).
@@ -185,6 +185,10 @@ add_filter( 'woocommerce_register_log_handlers', 'my_wc_log_handlers' );
You may want to create your own log handler class in order to send logs somewhere else, such as a Slack channel or perhaps an InfluxDB instance. Your class must extend the [`WC_Log_Handler`](https://woocommerce.github.io/code-reference/classes/WC-Log-Handler.html) abstract class and implement the [`WC_Log_Handler_Interface`](https://woocommerce.github.io/code-reference/classes/WC-Log-Handler-Interface.html) interface. The [`WC_Log_Handler_Email`](https://github.com/woocommerce/woocommerce/blob/6688c60fe47ad42d49deedab8be971288e4786c1/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-email.php) handler class provides a good example of how to set this up.
+### Log file storage location
+
+When using the "file system" log handler, by default the log files are stored in the `wc-logs` subdirectory of the WordPress `uploads` directory, which means they might be publicly accessible. WooCommerce adds an `.htaccess` file to prevent access to `wc-logs`, but not all web servers recognize that file. If you have the option, you may want to consider storing your log files in a directory outside of the web root. Make sure the directory has the same user/group permissions as the `uploads` directory so that WordPress can access it. Then use the `woocommerce_log_directory` filter hook to set the path to your custom directory.
+
### Turning off noisy logs
If there is a particular log that is recurring frequently and clogging up your log files, you should probably figure out why it keeps getting triggered and resolve the issue. However, if that's not possible, you can add a callback to the `woocommerce_logger_log_message` filter hook to ignore that particular log while still allowing other logs to get through:
diff --git a/docs/product-editor-development/product-editor.md b/docs/product-editor-development/product-editor.md
index 36f9b9fed73..d6233925b36 100644
--- a/docs/product-editor-development/product-editor.md
+++ b/docs/product-editor-development/product-editor.md
@@ -39,3 +39,4 @@ Please note that this check is currently not being enforced: the product editor
- [Examples on Template API usage](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/ProductTemplates/README.md/)
- [Related hooks and Template API documentation](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Admin/BlockTemplates/README.md)
- [Generic blocks documentation](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/blocks/generic/README.md)
+- [Validations and error handling](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/contexts/validation-context/README.md)
diff --git a/package.json b/package.json
index 6fba8a05ada..dc8abb9f7ee 100644
--- a/package.json
+++ b/package.json
@@ -22,8 +22,8 @@
"test": "pnpm -r test",
"lint": "pnpm -r lint",
"cherry-pick": "node ./tools/cherry-pick/bin/run",
- "clean": "rimraf -g '**/node_modules' '**/.wireit' && pnpm store prune && pnpm i",
- "distclean": "git clean --force -d -X",
+ "clean": "rimraf -g '**/node_modules' '**/.wireit' && pnpm store prune",
+ "buildclean": "git clean --force -d -X ./packages ./plugins ./tools",
"preinstall": "npx only-allow pnpm",
"postinstall": "pnpm git:update-hooks",
"git:update-hooks": "if test -d .git; then rm -rf .git/hooks && mkdir -p .git/hooks && husky install; else husky install; fi",
@@ -75,6 +75,9 @@
"@types/react": "^17.0.71",
"react-resize-aware": "3.1.1",
"@automattic/tour-kit>@wordpress/element": "4.4.1"
+ },
+ "patchedDependencies": {
+ "@wordpress/edit-site@5.15.0": "bin/patches/@wordpress__edit-site@5.15.0.patch"
}
}
}
diff --git a/packages/js/experimental/changelog/47385-dev-update-pnpm9-1 b/packages/js/admin-layout/changelog/dev-updates-to-packages-for-dependencies
similarity index 60%
rename from packages/js/experimental/changelog/47385-dev-update-pnpm9-1
rename to packages/js/admin-layout/changelog/dev-updates-to-packages-for-dependencies
index 92cf23a437f..213a23df05a 100644
--- a/packages/js/experimental/changelog/47385-dev-update-pnpm9-1
+++ b/packages/js/admin-layout/changelog/dev-updates-to-packages-for-dependencies
@@ -1,4 +1,4 @@
Significance: patch
Type: dev
-Update pnpm to 9.1.0
\ No newline at end of file
+Update dependencies
diff --git a/packages/js/api-core-tests/tests/shipping/shipping-method.test.js b/packages/js/api-core-tests/tests/shipping/shipping-method.test.js
index f3fca130ffb..68df6fe31e7 100644
--- a/packages/js/api-core-tests/tests/shipping/shipping-method.test.js
+++ b/packages/js/api-core-tests/tests/shipping/shipping-method.test.js
@@ -39,10 +39,7 @@ describe( 'Shipping methods API tests', () => {
expect( body.method_id ).toEqual( methodId );
expect( body.method_title ).toEqual( methodTitle );
expect( body.enabled ).toEqual( true );
-
- if ( [ 'flat_rate', 'local_pickup' ].includes( methodId ) ) {
- expect( body.settings.cost.value ).toEqual( cost );
- }
+ expect( body.settings.cost.value || '' ).toEqual( cost || '' );
// Cleanup: Delete the shipping method
await shippingMethodsApi.delete.shippingMethod(
diff --git a/packages/js/api/README.md b/packages/js/api/README.md
index e014365465f..aeb0c3c996f 100644
--- a/packages/js/api/README.md
+++ b/packages/js/api/README.md
@@ -10,9 +10,9 @@ features:
_\* TypeScript Definitions and Repositories are currently only supported for [Products](https://woocommerce.github.io/woocommerce-rest-api-docs/#products), and partially supported for [Orders](https://woocommerce.github.io/woocommerce-rest-api-docs/#orders)._
-## Differences from @woocommerce/woocomerce-rest-api
+## Differences from @woocommerce/woocommerce-rest-api
-WooCommerce has two API clients in JavaScript for interacting with a WooCommerce installation's RESTful API. This package, and the [@woocommerce/woocomerce-rest-api](https://www.npmjs.com/package/@woocommerce/woocommerce-rest-api) package.
+WooCommerce has two API clients in JavaScript for interacting with a WooCommerce installation's RESTful API. This package, and the [@woocommerce/woocommerce-rest-api](https://www.npmjs.com/package/@woocommerce/woocommerce-rest-api) package.
The main difference between them is the Repositories and the TypeScript definitions for the supported endpoints. When using Axios directly, as you can do with both libraries, you query the WooCommerce API in a raw object format, following the [API documentation](https://woocommerce.github.io/woocommerce-rest-api-docs/#introduction) parameters. Comparatively, with the Repositories provided in this package, you have the parameters as properties of an object, which gives you the benefits of auto-complete and strict types, for instance.
@@ -104,7 +104,7 @@ The following methods are available on all repositories if the corresponding met
- `read( objectId )` - Read a single object of the model type
- `update( objectId, {...properties} )` - Update a single object of the model type
-#### Child Repositories
+#### Child Repositories Use
In child model repositories, each method requires the `parentId` as the first parameter:
diff --git a/packages/js/experimental/changelog/fix-37502 b/packages/js/api/changelog/50156-patch-9
similarity index 52%
rename from packages/js/experimental/changelog/fix-37502
rename to packages/js/api/changelog/50156-patch-9
index 115429c4b01..b0d6fbdf8cb 100644
--- a/packages/js/experimental/changelog/fix-37502
+++ b/packages/js/api/changelog/50156-patch-9
@@ -1,4 +1,4 @@
Significance: patch
Type: tweak
+Comment: Fix typo (README.md)
-Correct spelling errors
diff --git a/packages/js/onboarding/changelog/47385-dev-update-pnpm9-1 b/packages/js/block-templates/changelog/dev-updates-to-packages-for-dependencies
similarity index 60%
rename from packages/js/onboarding/changelog/47385-dev-update-pnpm9-1
rename to packages/js/block-templates/changelog/dev-updates-to-packages-for-dependencies
index 92cf23a437f..213a23df05a 100644
--- a/packages/js/onboarding/changelog/47385-dev-update-pnpm9-1
+++ b/packages/js/block-templates/changelog/dev-updates-to-packages-for-dependencies
@@ -1,4 +1,4 @@
Significance: patch
Type: dev
-Update pnpm to 9.1.0
\ No newline at end of file
+Update dependencies
diff --git a/packages/js/components/changelog/dev-monorepo-caching-deps-per-target-package b/packages/js/components/changelog/dev-monorepo-caching-deps-per-target-package
new file mode 100644
index 00000000000..23d4a85f6b2
--- /dev/null
+++ b/packages/js/components/changelog/dev-monorepo-caching-deps-per-target-package
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+CI: added a missing dev-dependency for passing tests in updated CI environment.
diff --git a/packages/js/components/changelog/fix-add-escape-handle b/packages/js/components/changelog/fix-add-escape-handle
new file mode 100644
index 00000000000..a5e69d5941d
--- /dev/null
+++ b/packages/js/components/changelog/fix-add-escape-handle
@@ -0,0 +1,5 @@
+Significance: patch
+Type: fix
+Comment: Fix missing onEscape handle in SelectTree
+
+
diff --git a/packages/js/components/changelog/fix-misspelling-in-experimental-select-control-test b/packages/js/components/changelog/fix-misspelling-in-experimental-select-control-test
new file mode 100644
index 00000000000..398f33065db
--- /dev/null
+++ b/packages/js/components/changelog/fix-misspelling-in-experimental-select-control-test
@@ -0,0 +1,4 @@
+Significance: minor
+Type: tweak
+
+Fix typo in experimental select control tests
diff --git a/packages/js/components/changelog/update-abrev-global-unique-id b/packages/js/components/changelog/update-abrev-global-unique-id
new file mode 100644
index 00000000000..fa65896f52c
--- /dev/null
+++ b/packages/js/components/changelog/update-abrev-global-unique-id
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Allow adding HTML to label prop in label component
diff --git a/packages/js/components/changelog/update-navigation-while-on-input b/packages/js/components/changelog/update-navigation-while-on-input
new file mode 100644
index 00000000000..169cc6b8f5e
--- /dev/null
+++ b/packages/js/components/changelog/update-navigation-while-on-input
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Update SelectTree and Tree controls to allow highlighting items without focus
diff --git a/packages/js/components/changelog/update-select-tree-improvements-3 b/packages/js/components/changelog/update-select-tree-improvements-3
new file mode 100644
index 00000000000..c9a2dd88498
--- /dev/null
+++ b/packages/js/components/changelog/update-select-tree-improvements-3
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+SelectTree: allow navigation between items and input using tab and arrow keys
diff --git a/packages/js/components/package.json b/packages/js/components/package.json
index 52a75473295..fbb42628467 100644
--- a/packages/js/components/package.json
+++ b/packages/js/components/package.json
@@ -171,6 +171,7 @@
"jest-cli": "~27.5.1",
"postcss": "^8.4.32",
"postcss-loader": "^4.3.0",
+ "qrcode.react": "^3.1.0",
"react": "^17.0.2",
"rimraf": "5.0.5",
"sass-loader": "^10.5.0",
diff --git a/packages/js/components/src/experimental-select-control/selected-items.tsx b/packages/js/components/src/experimental-select-control/selected-items.tsx
index 217d8428a34..acdff11f0be 100644
--- a/packages/js/components/src/experimental-select-control/selected-items.tsx
+++ b/packages/js/components/src/experimental-select-control/selected-items.tsx
@@ -2,14 +2,23 @@
* External dependencies
*/
import classnames from 'classnames';
-import { createElement } from '@wordpress/element';
+import {
+ createElement,
+ forwardRef,
+ useImperativeHandle,
+ useRef,
+} from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
*/
import Tag from '../tag';
-import { getItemLabelType, getItemValueType } from './types';
+import {
+ getItemLabelType,
+ getItemValueType,
+ SelectedItemFocusHandle,
+} from './types';
type SelectedItemsProps< ItemType > = {
isReadOnly: boolean;
@@ -22,16 +31,23 @@ type SelectedItemsProps< ItemType > = {
[ key: string ]: string;
};
onRemove: ( item: ItemType ) => void;
+ onBlur?: ( event: React.FocusEvent ) => void;
+ onSelectedItemsEnd?: () => void;
};
-export const SelectedItems = < ItemType, >( {
- isReadOnly,
- items,
- getItemLabel,
- getItemValue,
- getSelectedItemProps,
- onRemove,
-}: SelectedItemsProps< ItemType > ) => {
+const PrivateSelectedItems = < ItemType, >(
+ {
+ isReadOnly,
+ items,
+ getItemLabel,
+ getItemValue,
+ getSelectedItemProps,
+ onRemove,
+ onBlur,
+ onSelectedItemsEnd,
+ }: SelectedItemsProps< ItemType >,
+ ref: React.ForwardedRef< SelectedItemFocusHandle >
+) => {
const classes = classnames(
'woocommerce-experimental-select-control__selected-items',
{
@@ -39,6 +55,16 @@ export const SelectedItems = < ItemType, >( {
}
);
+ const lastRemoveButtonRef = useRef< HTMLButtonElement >( null );
+
+ useImperativeHandle(
+ ref,
+ () => {
+ return () => lastRemoveButtonRef.current?.focus();
+ },
+ []
+ );
+
if ( isReadOnly ) {
return (
@@ -51,6 +77,25 @@ export const SelectedItems = < ItemType, >( {
);
}
+ const focusSibling = ( event: React.KeyboardEvent< HTMLDivElement > ) => {
+ const selectedItem = ( event.target as HTMLElement ).closest(
+ '.woocommerce-experimental-select-control__selected-item'
+ );
+ const sibling =
+ event.key === 'ArrowLeft' || event.key === 'Backspace'
+ ? selectedItem?.previousSibling
+ : selectedItem?.nextSibling;
+ if ( sibling ) {
+ (
+ ( sibling as HTMLElement ).querySelector(
+ '.woocommerce-tag__remove'
+ ) as HTMLElement
+ )?.focus();
+ return true;
+ }
+ return false;
+ };
+
return (
{ items.map( ( item, index ) => {
@@ -71,6 +116,30 @@ export const SelectedItems = < ItemType, >( {
onClick={ ( event ) => {
event.preventDefault();
} }
+ onKeyDown={ ( event ) => {
+ if (
+ event.key === 'ArrowLeft' ||
+ event.key === 'ArrowRight'
+ ) {
+ const focused = focusSibling( event );
+ if (
+ ! focused &&
+ event.key === 'ArrowRight' &&
+ onSelectedItemsEnd
+ ) {
+ onSelectedItemsEnd();
+ }
+ } else if (
+ event.key === 'ArrowUp' ||
+ event.key === 'ArrowDown'
+ ) {
+ event.preventDefault(); // prevent unwanted scroll
+ } else if ( event.key === 'Backspace' ) {
+ onRemove( item );
+ focusSibling( event );
+ }
+ } }
+ onBlur={ onBlur }
>
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
{ /* @ts-ignore Additional props are not required. */ }
@@ -78,6 +147,11 @@ export const SelectedItems = < ItemType, >( {
id={ getItemValue( item ) }
remove={ () => () => onRemove( item ) }
label={ getItemLabel( item ) }
+ ref={
+ index === items.length - 1
+ ? lastRemoveButtonRef
+ : undefined
+ }
/>
);
@@ -85,3 +159,9 @@ export const SelectedItems = < ItemType, >( {
);
};
+
+export const SelectedItems = forwardRef( PrivateSelectedItems ) as < ItemType >(
+ props: SelectedItemsProps< ItemType > & {
+ ref?: React.ForwardedRef< SelectedItemFocusHandle >;
+ }
+) => ReturnType< typeof PrivateSelectedItems >;
diff --git a/packages/js/components/src/experimental-select-control/test/use-async-filter.spec.ts b/packages/js/components/src/experimental-select-control/test/use-async-filter.spec.ts
index 846f407ca3f..a1893db647d 100644
--- a/packages/js/components/src/experimental-select-control/test/use-async-filter.spec.ts
+++ b/packages/js/components/src/experimental-select-control/test/use-async-filter.spec.ts
@@ -49,7 +49,7 @@ describe( 'useAsyncFilter', () => {
expect( filter ).toHaveBeenCalledWith( inputValue );
} );
- it( 'should trigger onFilterStart at the begining of the filtering', async () => {
+ it( 'should trigger onFilterStart at the beginning of the filtering', async () => {
const filteredItems: string[] = [];
onFilterStart.mockImplementation( ( value = '' ) => {
@@ -78,7 +78,7 @@ describe( 'useAsyncFilter', () => {
expect( filter ).toHaveBeenCalledWith( inputValue );
} );
- it( 'should trigger onFilterEnd when filtering is fullfiled', async () => {
+ it( 'should trigger onFilterEnd when filtering is fulfilled', async () => {
const filteredItems: string[] = [];
filter.mockResolvedValue( filteredItems );
diff --git a/packages/js/components/src/experimental-select-control/types.ts b/packages/js/components/src/experimental-select-control/types.ts
index 331b2d148b8..00aee79a11b 100644
--- a/packages/js/components/src/experimental-select-control/types.ts
+++ b/packages/js/components/src/experimental-select-control/types.ts
@@ -58,3 +58,5 @@ export type getItemLabelType< ItemType > = ( item: ItemType | null ) => string;
export type getItemValueType< ItemType > = (
item: ItemType | null
) => string | number;
+
+export type SelectedItemFocusHandle = () => void;
diff --git a/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx b/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx
index 1b7267389b4..b1c9dd4d1ba 100644
--- a/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx
+++ b/packages/js/components/src/experimental-select-tree-control/select-tree-menu.tsx
@@ -10,6 +10,7 @@ import {
useLayoutEffect,
useState,
} from '@wordpress/element';
+import { escapeRegExp } from 'lodash';
/**
* Internal dependencies
@@ -26,6 +27,7 @@ type MenuProps = {
isLoading?: boolean;
position?: Popover.Position;
scrollIntoViewOnOpen?: boolean;
+ highlightedIndex?: number;
items: LinkedTree[];
treeRef?: React.ForwardedRef< HTMLOListElement >;
onClose?: () => void;
@@ -44,6 +46,7 @@ export const SelectTreeMenu = ( {
onEscape,
shouldShowCreateButton,
onFirstItemLoop,
+ onExpand,
...props
}: MenuProps ) => {
const [ boundingRect, setBoundingRect ] = useState< DOMRect >();
@@ -66,7 +69,7 @@ export const SelectTreeMenu = ( {
// Scroll the selected item into view when the menu opens.
useEffect( () => {
if ( isOpen && scrollIntoViewOnOpen ) {
- selectControlMenuRef.current?.scrollIntoView();
+ selectControlMenuRef.current?.scrollIntoView?.();
}
}, [ isOpen, scrollIntoViewOnOpen ] );
@@ -74,9 +77,10 @@ export const SelectTreeMenu = ( {
if ( ! props.createValue || ! item.children?.length ) return false;
return item.children.some( ( child ) => {
if (
- new RegExp( props.createValue || '', 'ig' ).test(
- child.data.label
- )
+ new RegExp(
+ escapeRegExp( props.createValue || '' ),
+ 'ig'
+ ).test( child.data.label )
) {
return true;
}
@@ -130,6 +134,7 @@ export const SelectTreeMenu = ( {
ref={ ref }
items={ items }
onTreeBlur={ onClose }
+ onExpand={ onExpand }
shouldItemBeExpanded={
shouldItemBeExpanded
}
diff --git a/packages/js/components/src/experimental-select-tree-control/select-tree.tsx b/packages/js/components/src/experimental-select-tree-control/select-tree.tsx
index c23a64d0ffd..ed5d7b09b06 100644
--- a/packages/js/components/src/experimental-select-tree-control/select-tree.tsx
+++ b/packages/js/components/src/experimental-select-tree-control/select-tree.tsx
@@ -8,6 +8,7 @@ import {
useEffect,
useState,
Fragment,
+ useRef,
} from '@wordpress/element';
import { useInstanceId } from '@wordpress/compose';
import { BaseControl, Button, TextControl } from '@wordpress/components';
@@ -18,13 +19,23 @@ import { speak } from '@wordpress/a11y';
/**
* Internal dependencies
*/
-import { useLinkedTree } from '../experimental-tree-control/hooks/use-linked-tree';
-import { Item, TreeControlProps } from '../experimental-tree-control/types';
+import {
+ toggleNode,
+ createLinkedTree,
+ getVisibleNodeIndex as getVisibleNodeIndex,
+ getNodeDataByIndex,
+} from '../experimental-tree-control/linked-tree-utils';
+import {
+ Item,
+ LinkedTree,
+ TreeControlProps,
+} from '../experimental-tree-control/types';
import { SelectedItems } from '../experimental-select-control/selected-items';
import { ComboBox } from '../experimental-select-control/combo-box';
import { SuffixIcon } from '../experimental-select-control/suffix-icon';
import { SelectTreeMenu } from './select-tree-menu';
import { escapeHTML } from '../utils';
+import { SelectedItemFocusHandle } from '../experimental-select-control/types';
interface SelectTreeProps extends TreeControlProps {
id: string;
@@ -48,12 +59,22 @@ export const SelectTree = function SelectTree( {
initialInputValue,
onInputChange,
shouldShowCreateButton,
- help = __( 'Separate with commas or the Enter key.', 'woocommerce' ),
+ help,
isClearingAllowed = false,
onClear = () => {},
...props
}: SelectTreeProps ) {
- const linkedTree = useLinkedTree( items );
+ const [ linkedTree, setLinkedTree ] = useState< LinkedTree[] >( [] );
+ const [ highlightedIndex, setHighlightedIndex ] = useState( -1 );
+
+ // whenever the items change, the linked tree needs to be recalculated
+ useEffect( () => {
+ setLinkedTree( createLinkedTree( items, props.createValue ) );
+ }, [ items.length ] );
+
+ // reset highlighted index when the input value changes
+ useEffect( () => setHighlightedIndex( -1 ), [ props.createValue ] );
+
const selectTreeInstanceId = useInstanceId(
SelectTree,
'woocommerce-experimental-select-tree-control__dropdown'
@@ -63,6 +84,8 @@ export const SelectTree = function SelectTree( {
'woocommerce-select-tree-control__menu'
) as string;
+ const selectedItemsFocusHandle = useRef< SelectedItemFocusHandle >( null );
+
function isEventOutside( event: React.FocusEvent ) {
const isInsideSelect = document
.getElementById( selectTreeInstanceId )
@@ -74,7 +97,10 @@ export const SelectTree = function SelectTree( {
'.woocommerce-experimental-select-tree-control__popover-menu'
)
?.contains( event.relatedTarget );
- return ! ( isInsideSelect || isInsidePopover );
+ const isInRemoveTag = event.relatedTarget?.classList.contains(
+ 'woocommerce-tag__remove'
+ );
+ return ! isInsideSelect && ! isInRemoveTag && ! isInsidePopover;
}
const recalculateInputValue = () => {
@@ -104,6 +130,19 @@ export const SelectTree = function SelectTree( {
}
}, [ isFocused ] );
+ // Scroll the newly highlighted item into view
+ useEffect(
+ () =>
+ document
+ .querySelector(
+ '.experimental-woocommerce-tree-item--highlighted'
+ )
+ ?.scrollIntoView?.( {
+ block: 'nearest',
+ } ),
+ [ highlightedIndex ]
+ );
+
let placeholder: string | undefined = '';
if ( Array.isArray( props.selected ) ) {
placeholder = props.selected.length === 0 ? props.placeholder : '';
@@ -111,12 +150,30 @@ export const SelectTree = function SelectTree( {
placeholder = props.placeholder;
}
+ // reset highlighted index when the input value changes
+ useEffect( () => {
+ if (
+ highlightedIndex === items.length &&
+ ! shouldShowCreateButton?.( props.createValue )
+ ) {
+ setHighlightedIndex( items.length - 1 );
+ }
+ }, [ props.createValue ] );
+
const inputProps: React.InputHTMLAttributes< HTMLInputElement > = {
className: 'woocommerce-experimental-select-control__input',
id: `${ props.id }-input`,
'aria-autocomplete': 'list',
- 'aria-controls': `${ props.id }-menu`,
+ 'aria-activedescendant':
+ highlightedIndex >= 0
+ ? `woocommerce-experimental-tree-control__menu-item-${ highlightedIndex }`
+ : undefined,
+ 'aria-controls': menuInstanceId,
+ 'aria-owns': menuInstanceId,
+ role: 'combobox',
autoComplete: 'off',
+ 'aria-expanded': isOpen,
+ 'aria-haspopup': 'tree',
disabled,
onFocus: ( event ) => {
if ( props.multiple ) {
@@ -141,44 +198,132 @@ export const SelectTree = function SelectTree( {
}
},
onBlur: ( event ) => {
- if ( isOpen && isEventOutside( event ) ) {
+ event.preventDefault();
+ if ( isEventOutside( event ) ) {
setIsOpen( false );
+ setIsFocused( false );
recalculateInputValue();
}
- setIsFocused( false );
},
onKeyDown: ( event ) => {
setIsOpen( true );
if ( event.key === 'ArrowDown' ) {
event.preventDefault();
- // focus on the first element from the Popover
- (
- document.querySelector(
- `#${ menuInstanceId } input, #${ menuInstanceId } button`
- ) as HTMLInputElement | HTMLButtonElement
- )?.focus();
- }
- if ( event.key === 'Tab' || event.key === 'Escape' ) {
+ if (
+ // is advancing from the last menu item to the create button
+ highlightedIndex === items.length - 1 &&
+ shouldShowCreateButton?.( props.createValue )
+ ) {
+ setHighlightedIndex( items.length );
+ } else {
+ const visibleNodeIndex = getVisibleNodeIndex(
+ linkedTree,
+ Math.min( highlightedIndex + 1, items.length ),
+ 'down'
+ );
+ if ( visibleNodeIndex !== undefined ) {
+ setHighlightedIndex( visibleNodeIndex );
+ }
+ }
+ } else if ( event.key === 'ArrowUp' ) {
+ event.preventDefault();
+ if ( highlightedIndex > 0 ) {
+ const visibleNodeIndex = getVisibleNodeIndex(
+ linkedTree,
+ Math.max( highlightedIndex - 1, -1 ),
+ 'up'
+ );
+ if ( visibleNodeIndex !== undefined ) {
+ setHighlightedIndex( visibleNodeIndex );
+ }
+ } else {
+ setHighlightedIndex( -1 );
+ }
+ } else if ( event.key === 'Tab' || event.key === 'Escape' ) {
setIsOpen( false );
recalculateInputValue();
- }
- if ( event.key === ',' || event.key === 'Enter' ) {
+ } else if ( event.key === 'Enter' || event.key === ',' ) {
event.preventDefault();
- const item = items.find(
- ( i ) => i.label === escapeHTML( inputValue )
- );
- const isAlreadySelected =
- Array.isArray( props.selected ) &&
- Boolean(
- props.selected.find(
- ( i ) => i.label === escapeHTML( inputValue )
- )
+ if (
+ highlightedIndex === items.length &&
+ shouldShowCreateButton
+ ) {
+ props.onCreateNew?.();
+ } else if (
+ // is selecting an item
+ highlightedIndex !== -1
+ ) {
+ const nodeData = getNodeDataByIndex(
+ linkedTree,
+ highlightedIndex
);
- if ( props.onSelect && item && ! isAlreadySelected ) {
- props.onSelect( item );
- setInputValue( '' );
- recalculateInputValue();
+ if ( ! nodeData ) {
+ return;
+ }
+ if ( props.multiple && Array.isArray( props.selected ) ) {
+ if (
+ ! Boolean(
+ props.selected.find(
+ ( i ) => i.label === nodeData.label
+ )
+ )
+ ) {
+ if ( props.onSelect ) {
+ props.onSelect( nodeData );
+ }
+ } else if ( props.onRemove ) {
+ props.onRemove( nodeData );
+ }
+ setInputValue( '' );
+ } else {
+ onInputChange?.( nodeData.label );
+ props.onSelect?.( nodeData );
+ setIsOpen( false );
+ setIsFocused( false );
+ focusOnInput();
+ }
+ } else if ( inputValue ) {
+ // no highlighted item, but there is an input value, check if it matches any item
+
+ const item = items.find(
+ ( i ) => i.label === escapeHTML( inputValue )
+ );
+ const isAlreadySelected = Array.isArray( props.selected )
+ ? Boolean(
+ props.selected.find(
+ ( i ) =>
+ i.label === escapeHTML( inputValue )
+ )
+ )
+ : props.selected?.label === escapeHTML( inputValue );
+ if ( item && ! isAlreadySelected ) {
+ props.onSelect?.( item );
+ setInputValue( '' );
+ recalculateInputValue();
+ }
}
+ } else if (
+ event.key === 'Backspace' &&
+ // test if the cursor is at the beginning of the input with nothing selected
+ ( event.target as HTMLInputElement ).selectionStart === 0 &&
+ ( event.target as HTMLInputElement ).selectionEnd === 0 &&
+ selectedItemsFocusHandle.current
+ ) {
+ selectedItemsFocusHandle.current();
+ } else if ( event.key === 'ArrowRight' ) {
+ setLinkedTree(
+ toggleNode( linkedTree, highlightedIndex, true )
+ );
+ } else if ( event.key === 'ArrowLeft' ) {
+ setLinkedTree(
+ toggleNode( linkedTree, highlightedIndex, false )
+ );
+ } else if ( event.key === 'Home' ) {
+ event.preventDefault();
+ setHighlightedIndex( 0 );
+ } else if ( event.key === 'End' ) {
+ event.preventDefault();
+ setHighlightedIndex( items.length - 1 );
}
},
onChange: ( event ) => {
@@ -219,7 +364,14 @@ export const SelectTree = function SelectTree( {
<>
{ props.multiple ? (
@@ -227,16 +379,18 @@ export const SelectTree = function SelectTree( {
comboBoxProps={ {
className:
'woocommerce-experimental-select-control__combo-box-wrapper',
- role: 'combobox',
- 'aria-expanded': isOpen,
- 'aria-haspopup': 'tree',
- 'aria-owns': `${ props.id }-menu`,
} }
inputProps={ inputProps }
suffix={
{ isClearingAllowed && isOpen && (
-
+
item?.label || ''
}
@@ -262,12 +421,20 @@ export const SelectTree = function SelectTree( {
}
onRemove={ ( item ) => {
if (
+ item &&
! Array.isArray( item ) &&
props.onRemove
) {
props.onRemove( item );
}
} }
+ onBlur={ ( event ) => {
+ if ( isEventOutside( event ) ) {
+ setIsOpen( false );
+ setIsFocused( false );
+ }
+ } }
+ onSelectedItemsEnd={ focusOnInput }
getSelectedItemProps={ () => ( {} ) }
/>
@@ -311,8 +478,18 @@ export const SelectTree = function SelectTree( {
isEventOutside={ isEventOutside }
isLoading={ isLoading }
isOpen={ isOpen }
+ highlightedIndex={ highlightedIndex }
+ onExpand={ ( index, value ) => {
+ setLinkedTree(
+ toggleNode( linkedTree, index, value )
+ );
+ } }
items={ linkedTree }
shouldShowCreateButton={ shouldShowCreateButton }
+ onEscape={ () => {
+ focusOnInput();
+ setIsOpen( false );
+ } }
onClose={ () => {
setIsOpen( false );
} }
diff --git a/packages/js/components/src/experimental-select-tree-control/test/select-tree.test.tsx b/packages/js/components/src/experimental-select-tree-control/test/select-tree.test.tsx
index b6f9dd266fb..07a8694897e 100644
--- a/packages/js/components/src/experimental-select-tree-control/test/select-tree.test.tsx
+++ b/packages/js/components/src/experimental-select-tree-control/test/select-tree.test.tsx
@@ -1,4 +1,6 @@
import { render } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { useState } from 'react';
import React, { createElement } from '@wordpress/element';
import { SelectTree } from '../select-tree';
import { Item } from '../../experimental-tree-control';
@@ -26,6 +28,44 @@ const DEFAULT_PROPS = {
placeholder: 'Type here',
};
+const TestComponent = ( { multiple }: { multiple?: boolean } ) => {
+ const [ typedValue, setTypedValue ] = useState( '' );
+ const [ selected, setSelected ] = useState< any >( [] );
+
+ return createElement( SelectTree, {
+ ...DEFAULT_PROPS,
+ multiple,
+ shouldShowCreateButton: () => true,
+ onInputChange: ( value ) => {
+ setTypedValue( value || '' );
+ },
+ createValue: typedValue,
+ selected: Array.isArray( selected )
+ ? selected.map( ( i ) => ( {
+ value: String( i.id ),
+ label: i.name,
+ } ) )
+ : {
+ value: String( selected.id ),
+ label: selected.name,
+ },
+ onSelect: ( item: Item | Item[] ) =>
+ item && Array.isArray( item )
+ ? setSelected(
+ item.map( ( i ) => ( {
+ id: +i.value,
+ name: i.label,
+ parent: i.parent ? +i.parent : 0,
+ } ) )
+ )
+ : setSelected( {
+ id: +item.value,
+ name: item.label,
+ parent: item.parent ? +item.parent : 0,
+ } ),
+ } );
+};
+
describe( 'SelectTree', () => {
beforeEach( () => {
jest.clearAllMocks();
@@ -36,7 +76,7 @@ describe( 'SelectTree', () => {
);
expect( queryByText( 'Item 1' ) ).not.toBeInTheDocument();
- queryByRole( 'textbox' )?.focus();
+ queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Item 1' ) ).toBeInTheDocument();
} );
@@ -47,20 +87,21 @@ describe( 'SelectTree', () => {
shouldShowCreateButton={ () => true }
/>
);
- queryByRole( 'textbox' )?.focus();
+ queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Create new' ) ).toBeInTheDocument();
} );
it( 'should not show create button when callback is false or no callback', () => {
const { queryByText, queryByRole } = render(
);
- queryByRole( 'textbox' )?.focus();
+ queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Create new' ) ).not.toBeInTheDocument();
} );
it( 'should show a root item when focused and child when expand button is clicked', () => {
- const { queryByText, queryByLabelText, queryByRole } =
- render( );
- queryByRole( 'textbox' )?.focus();
+ const { queryByText, queryByLabelText, queryByRole } = render(
+
+ );
+ queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Item 1' ) ).toBeInTheDocument();
expect( queryByText( 'Item 2' ) ).not.toBeInTheDocument();
@@ -72,7 +113,7 @@ describe( 'SelectTree', () => {
const { queryAllByRole, queryByRole } = render(
);
- queryByRole( 'textbox' )?.focus();
+ queryByRole( 'combobox' )?.focus();
expect( queryAllByRole( 'treeitem' )[ 0 ] ).toHaveAttribute(
'aria-selected',
'true'
@@ -87,7 +128,7 @@ describe( 'SelectTree', () => {
shouldShowCreateButton={ () => true }
/>
);
- queryByRole( 'textbox' )?.focus();
+ queryByRole( 'combobox' )?.focus();
expect( queryByText( 'Create "new item"' ) ).toBeInTheDocument();
} );
it( 'should call onCreateNew when Create "" button is clicked', () => {
@@ -100,8 +141,34 @@ describe( 'SelectTree', () => {
onCreateNew={ mockFn }
/>
);
- queryByRole( 'textbox' )?.focus();
+ queryByRole( 'combobox' )?.focus();
queryByText( 'Create "new item"' )?.click();
expect( mockFn ).toBeCalledTimes( 1 );
} );
+ it( 'correctly selects existing item in single mode with arrow keys', async () => {
+ const { findByRole } = render( );
+ const combobox = ( await findByRole( 'combobox' ) ) as HTMLInputElement;
+ combobox.focus();
+ userEvent.keyboard( '{arrowdown}{enter}' );
+ expect( combobox.value ).toBe( 'Item 1' );
+ } );
+ it( 'correctly selects existing item in single mode by typing and pressing Enter', async () => {
+ const { findByRole } = render( );
+ const combobox = ( await findByRole( 'combobox' ) ) as HTMLInputElement;
+ combobox.focus();
+ userEvent.keyboard( 'Item 1{enter}' );
+ userEvent.tab();
+ expect( combobox.value ).toBe( 'Item 1' );
+ } );
+ it( 'correctly selects existing item in multiple mode by typing and pressing Enter', async () => {
+ const { findByRole, getAllByText } = render(
+
+ );
+ const combobox = ( await findByRole( 'combobox' ) ) as HTMLInputElement;
+ combobox.focus();
+ userEvent.keyboard( 'Item 1' );
+ userEvent.keyboard( '{enter}' );
+ expect( combobox.value ).toBe( '' ); // input is cleared
+ expect( getAllByText( 'Item 1' )[ 0 ] ).toBeInTheDocument(); // item is selected (turns into a token)
+ } );
} );
diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-linked-tree.ts b/packages/js/components/src/experimental-tree-control/hooks/use-linked-tree.ts
deleted file mode 100644
index 94ff95706b8..00000000000
--- a/packages/js/components/src/experimental-tree-control/hooks/use-linked-tree.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * External dependencies
- */
-import { useMemo } from 'react';
-
-/**
- * Internal dependencies
- */
-import { Item, LinkedTree } from '../types';
-
-type MemoItems = {
- [ value: Item[ 'value' ] ]: LinkedTree;
-};
-
-function findChildren(
- items: Item[],
- parent?: Item[ 'parent' ],
- memo: MemoItems = {}
-): LinkedTree[] {
- const children: Item[] = [];
- const others: Item[] = [];
-
- items.forEach( ( item ) => {
- if ( item.parent === parent ) {
- children.push( item );
- } else {
- others.push( item );
- }
- memo[ item.value ] = {
- parent: undefined,
- data: item,
- children: [],
- };
- } );
-
- return children.map( ( child ) => {
- const linkedTree = memo[ child.value ];
- linkedTree.parent = child.parent ? memo[ child.parent ] : undefined;
- linkedTree.children = findChildren( others, child.value, memo );
- return linkedTree;
- } );
-}
-
-export function useLinkedTree( items: Item[] ): LinkedTree[] {
- const linkedTree = useMemo( () => {
- return findChildren( items, undefined, {} );
- }, [ items ] );
-
- return linkedTree;
-}
diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts b/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts
index 9d90fe406bb..3c1dba15f04 100644
--- a/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts
+++ b/packages/js/components/src/experimental-tree-control/hooks/use-tree-item.ts
@@ -31,6 +31,10 @@ export function useTreeItem( {
onLastItemLoop,
onFirstItemLoop,
onTreeBlur,
+ onEscape,
+ highlightedIndex,
+ isHighlighted,
+ onExpand,
...props
}: TreeItemProps ) {
const nextLevel = level + 1;
@@ -78,16 +82,19 @@ export function useTreeItem( {
getLabel,
treeItemProps: {
...props,
- role: 'none',
+ id:
+ 'woocommerce-experimental-tree-control__menu-item-' +
+ item.index,
+ role: 'option',
},
headingProps: {
role: 'treeitem',
'aria-selected': selection.checkedStatus !== 'unchecked',
'aria-expanded': item.children.length
- ? expander.isExpanded
+ ? item.data.isExpanded
: undefined,
'aria-owns':
- item.children.length && expander.isExpanded
+ item.children.length && item.data.isExpanded
? subTreeId
: undefined,
style: {
diff --git a/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts b/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts
index 393a02873cb..fc7c697ade5 100644
--- a/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts
+++ b/packages/js/components/src/experimental-tree-control/hooks/use-tree.ts
@@ -10,7 +10,7 @@ import { TreeProps } from '../types';
export function useTree( {
items,
level = 1,
- role = 'tree',
+ role = 'listbox',
multiple,
selected,
getItemLabel,
@@ -24,6 +24,9 @@ export function useTree( {
onCreateNew,
shouldShowCreateButton,
onFirstItemLoop,
+ onEscape,
+ highlightedIndex,
+ onExpand,
...props
}: TreeProps ) {
return {
diff --git a/packages/js/components/src/experimental-tree-control/linked-tree-utils.ts b/packages/js/components/src/experimental-tree-control/linked-tree-utils.ts
new file mode 100644
index 00000000000..38f33003411
--- /dev/null
+++ b/packages/js/components/src/experimental-tree-control/linked-tree-utils.ts
@@ -0,0 +1,211 @@
+/**
+ * Internal dependencies
+ */
+import { AugmentedItem, Item, LinkedTree } from './types';
+
+type MemoItems = {
+ [ value: AugmentedItem[ 'value' ] ]: LinkedTree;
+};
+
+const shouldItemBeExpanded = (
+ item: LinkedTree,
+ createValue: string | undefined
+): boolean => {
+ if ( ! createValue || ! item.children?.length ) return false;
+ return item.children.some( ( child ) => {
+ if ( new RegExp( createValue || '', 'ig' ).test( child.data.label ) ) {
+ return true;
+ }
+ return shouldItemBeExpanded( child, createValue );
+ } );
+};
+
+function findChildren(
+ items: AugmentedItem[],
+ memo: MemoItems = {},
+ parent?: AugmentedItem[ 'parent' ],
+ createValue?: string | undefined
+): LinkedTree[] {
+ const children: AugmentedItem[] = [];
+ const others: AugmentedItem[] = [];
+
+ items.forEach( ( item ) => {
+ if ( item.parent === parent ) {
+ children.push( item );
+ } else {
+ others.push( item );
+ }
+ memo[ item.value ] = {
+ parent: undefined,
+ data: item,
+ children: [],
+ };
+ } );
+
+ return children.map( ( child ) => {
+ const linkedTree = memo[ child.value ];
+ linkedTree.parent = child.parent ? memo[ child.parent ] : undefined;
+ linkedTree.children = findChildren(
+ others,
+ memo,
+ child.value,
+ createValue
+ );
+ linkedTree.data.isExpanded =
+ linkedTree.children.length === 0
+ ? true
+ : shouldItemBeExpanded( linkedTree, createValue );
+ return linkedTree;
+ } );
+}
+
+function populateIndexes(
+ linkedTree: LinkedTree[],
+ startCount = 0
+): LinkedTree[] {
+ let count = startCount;
+
+ function populate( tree: LinkedTree[] ): number {
+ for ( const node of tree ) {
+ node.index = count;
+ count++;
+ if ( node.children ) {
+ count = populate( node.children );
+ }
+ }
+ return count;
+ }
+
+ populate( linkedTree );
+ return linkedTree;
+}
+
+// creates a linked tree from an array of Items
+export function createLinkedTree(
+ items: Item[],
+ value: string | undefined
+): LinkedTree[] {
+ const augmentedItems = items.map( ( i ) => ( {
+ ...i,
+ isExpanded: false,
+ } ) );
+ return populateIndexes(
+ findChildren( augmentedItems, {}, undefined, value )
+ );
+}
+
+// Toggles the expanded state of a node in a linked tree
+export function toggleNode(
+ tree: LinkedTree[],
+ number: number,
+ value: boolean
+): LinkedTree[] {
+ return tree.map( ( node ) => {
+ return {
+ ...node,
+ children: node.children
+ ? toggleNode( node.children, number, value )
+ : node.children,
+ data: {
+ ...node.data,
+ isExpanded:
+ node.index === number ? value : node.data.isExpanded,
+ },
+ ...( node.parent
+ ? {
+ parent: {
+ ...node.parent,
+ data: {
+ ...node.parent.data,
+ isExpanded:
+ node.parent.index === number
+ ? value
+ : node.parent.data.isExpanded,
+ },
+ },
+ }
+ : {} ),
+ };
+ } );
+}
+
+// Gets the index of the next/previous visible node in the linked tree
+export function getVisibleNodeIndex(
+ tree: LinkedTree[],
+ highlightedIndex: number,
+ direction: 'up' | 'down'
+): number | undefined {
+ if ( direction === 'down' ) {
+ for ( const node of tree ) {
+ if ( ! node.parent || node.parent.data.isExpanded ) {
+ if (
+ node.index !== undefined &&
+ node.index >= highlightedIndex
+ ) {
+ return node.index;
+ }
+ const visibleNodeIndex = getVisibleNodeIndex(
+ node.children,
+ highlightedIndex,
+ direction
+ );
+ if ( visibleNodeIndex !== undefined ) {
+ return visibleNodeIndex;
+ }
+ }
+ }
+ } else {
+ for ( let i = tree.length - 1; i >= 0; i-- ) {
+ const node = tree[ i ];
+ if ( ! node.parent || node.parent.data.isExpanded ) {
+ const visibleNodeIndex = getVisibleNodeIndex(
+ node.children,
+ highlightedIndex,
+ direction
+ );
+ if ( visibleNodeIndex !== undefined ) {
+ return visibleNodeIndex;
+ }
+ if (
+ node.index !== undefined &&
+ node.index <= highlightedIndex
+ ) {
+ return node.index;
+ }
+ }
+ }
+ }
+
+ return undefined;
+}
+
+// Counts the number of nodes in a LinkedTree
+export function countNumberOfNodes( linkedTree: LinkedTree[] ) {
+ let count = 0;
+ for ( const node of linkedTree ) {
+ count++;
+ if ( node.children ) {
+ count += countNumberOfNodes( node.children );
+ }
+ }
+ return count;
+}
+
+// Gets the data of a node by its index
+export function getNodeDataByIndex(
+ linkedTree: LinkedTree[],
+ index: number
+): Item | undefined {
+ for ( const node of linkedTree ) {
+ if ( node.index === index ) {
+ return node.data;
+ }
+ if ( node.children ) {
+ const child = getNodeDataByIndex( node.children, index );
+ if ( child ) {
+ return child;
+ }
+ }
+ }
+ return undefined;
+}
diff --git a/packages/js/components/src/experimental-tree-control/tree-control.tsx b/packages/js/components/src/experimental-tree-control/tree-control.tsx
index 24a484a2995..d2e3db9db8e 100644
--- a/packages/js/components/src/experimental-tree-control/tree-control.tsx
+++ b/packages/js/components/src/experimental-tree-control/tree-control.tsx
@@ -6,7 +6,7 @@ import { createElement, forwardRef } from 'react';
/**
* Internal dependencies
*/
-import { useLinkedTree } from './hooks/use-linked-tree';
+import { createLinkedTree } from './linked-tree-utils';
import { Tree } from './tree';
import { TreeControlProps } from './types';
@@ -14,7 +14,7 @@ export const TreeControl = forwardRef( function ForwardedTree(
{ items, ...props }: TreeControlProps,
ref: React.ForwardedRef< HTMLOListElement >
) {
- const linkedTree = useLinkedTree( items );
+ const linkedTree = createLinkedTree( items, props.createValue );
return ;
} );
diff --git a/packages/js/components/src/experimental-tree-control/tree-item.scss b/packages/js/components/src/experimental-tree-control/tree-item.scss
index e0bd703c354..8e5f7a95e83 100644
--- a/packages/js/components/src/experimental-tree-control/tree-item.scss
+++ b/packages/js/components/src/experimental-tree-control/tree-item.scss
@@ -6,6 +6,8 @@ $control-size: $gap-large;
&--highlighted {
> .experimental-woocommerce-tree-item__heading {
background-color: $gray-100;
+ outline: 1.5px solid var( --wp-admin-theme-color );
+ outline-offset: -1.5px;
}
}
diff --git a/packages/js/components/src/experimental-tree-control/tree-item.tsx b/packages/js/components/src/experimental-tree-control/tree-item.tsx
index fcc7012aa11..afbfe870895 100644
--- a/packages/js/components/src/experimental-tree-control/tree-item.tsx
+++ b/packages/js/components/src/experimental-tree-control/tree-item.tsx
@@ -24,21 +24,25 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
treeItemProps,
headingProps,
treeProps,
- expander: { isExpanded, onToggleExpand },
selection,
- highlighter: { isHighlighted },
getLabel,
} = useTreeItem( {
...props,
ref,
} );
- function handleEscapePress(
- event: React.KeyboardEvent< HTMLInputElement >
- ) {
+ function handleKeyDown( event: React.KeyboardEvent< HTMLElement > ) {
if ( event.key === 'Escape' && props.onEscape ) {
event.preventDefault();
props.onEscape();
+ } else if ( event.key === 'ArrowLeft' ) {
+ if ( item.index !== undefined ) {
+ props.onExpand?.( item.index, false );
+ }
+ } else if ( event.key === 'ArrowRight' ) {
+ if ( item.index !== undefined ) {
+ props.onExpand?.( item.index, true );
+ }
}
}
@@ -50,7 +54,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
'experimental-woocommerce-tree-item',
{
'experimental-woocommerce-tree-item--highlighted':
- isHighlighted,
+ props.isHighlighted,
}
) }
>
@@ -67,7 +71,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
}
checked={ selection.checkedStatus === 'checked' }
onChange={ selection.onSelectChild }
- onKeyDown={ handleEscapePress }
+ onKeyDown={ handleKeyDown }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore __nextHasNoMarginBottom is a valid prop
__nextHasNoMarginBottom={ true }
@@ -80,7 +84,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
onChange={ ( event ) =>
selection.onSelectChild( event.target.checked )
}
- onKeyDown={ handleEscapePress }
+ onKeyDown={ handleKeyDown }
/>
) }
@@ -94,11 +98,21 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
{ Boolean( item.children?.length ) && (
{
+ if ( item.index !== undefined ) {
+ props.onExpand?.(
+ item.index,
+ ! item.data.isExpanded
+ );
+ }
+ } }
+ onKeyDown={ handleKeyDown }
className="experimental-woocommerce-tree-item__expander"
aria-label={
- isExpanded
+ item.data.isExpanded
? __( 'Collapse', 'woocommerce' )
: __( 'Expand', 'woocommerce' )
}
@@ -107,8 +121,13 @@ export const TreeItem = forwardRef( function ForwardedTreeItem(
) }
- { Boolean( item.children.length ) && isExpanded && (
-
+ { Boolean( item.children.length ) && item.data.isExpanded && (
+
) }
);
diff --git a/packages/js/components/src/experimental-tree-control/tree.scss b/packages/js/components/src/experimental-tree-control/tree.scss
index 7225539369d..341d3a225ab 100644
--- a/packages/js/components/src/experimental-tree-control/tree.scss
+++ b/packages/js/components/src/experimental-tree-control/tree.scss
@@ -16,8 +16,9 @@
width: 100%;
cursor: default;
&:hover,
- &:focus-within {
- outline: 1.5px solid var( --wp-admin-theme-color );
+ &:focus-within,
+ &--highlighted {
+ outline: 1.5px solid var(--wp-admin-theme-color);
outline-offset: -1.5px;
background-color: $gray-100;
}
diff --git a/packages/js/components/src/experimental-tree-control/tree.tsx b/packages/js/components/src/experimental-tree-control/tree.tsx
index 08957af0904..ccbe500829a 100644
--- a/packages/js/components/src/experimental-tree-control/tree.tsx
+++ b/packages/js/components/src/experimental-tree-control/tree.tsx
@@ -14,6 +14,7 @@ import { useMergeRefs } from '@wordpress/compose';
import { useTree } from './hooks/use-tree';
import { TreeItem } from './tree-item';
import { TreeProps } from './types';
+import { countNumberOfNodes } from './linked-tree-utils';
export const Tree = forwardRef( function ForwardedTree(
props: TreeProps,
@@ -27,6 +28,8 @@ export const Tree = forwardRef( function ForwardedTree(
ref,
} );
+ const numberOfItems = countNumberOfNodes( items );
+
const isCreateButtonVisible =
props.shouldShowCreateButton &&
props.shouldShowCreateButton( props.createValue );
@@ -45,7 +48,12 @@ export const Tree = forwardRef( function ForwardedTree(
{ items.map( ( child, index ) => (
{
(
rootListRef.current
- ?.closest( 'ol[role="tree"]' )
+ ?.closest( 'ol[role="listbox"]' )
?.parentElement?.querySelector(
'.experimental-woocommerce-tree__button'
) as HTMLButtonElement
@@ -67,7 +75,17 @@ export const Tree = forwardRef( function ForwardedTree(
) : null }
{ isCreateButtonVisible && (
{
if ( props.onCreateNew ) {
props.onCreateNew();
diff --git a/packages/js/components/src/experimental-tree-control/types.ts b/packages/js/components/src/experimental-tree-control/types.ts
index 6d759e3801e..185f0bd8f8c 100644
--- a/packages/js/components/src/experimental-tree-control/types.ts
+++ b/packages/js/components/src/experimental-tree-control/types.ts
@@ -4,10 +4,15 @@ export interface Item {
label: string;
}
+export type AugmentedItem = Item & {
+ isExpanded: boolean;
+};
+
export interface LinkedTree {
parent?: LinkedTree;
- data: Item;
+ data: AugmentedItem;
children: LinkedTree[];
+ index?: number;
}
export type CheckedStatus = 'checked' | 'unchecked' | 'indeterminate';
@@ -18,6 +23,11 @@ type BaseTreeProps = {
* a list of items if it is true.
*/
selected?: Item | Item[];
+
+ onExpand?( index: number, value: boolean ): void;
+
+ highlightedIndex?: number;
+
/**
* Whether the tree items are single or multiple selected.
*/
@@ -137,6 +147,7 @@ export type TreeItemProps = BaseTreeProps &
item: LinkedTree;
index: number;
isFocused?: boolean;
+ isHighlighted?: boolean;
getLabel?( item: LinkedTree ): JSX.Element;
shouldItemBeExpanded?( item: LinkedTree ): boolean;
onLastItemLoop?( event: React.KeyboardEvent< HTMLDivElement > ): void;
diff --git a/packages/js/components/src/search/test/index.js b/packages/js/components/src/search/test/index.js
index 45a05328dbd..10353bdba38 100644
--- a/packages/js/components/src/search/test/index.js
+++ b/packages/js/components/src/search/test/index.js
@@ -87,7 +87,7 @@ describe( 'Search', () => {
userEvent.type( getByRole( 'combobox' ), 'A' );
// Wait for async options processing.
await waitFor( () => {
- expect( optionsSpy ).toBeCalledWith( 'A' );
+ expect( optionsSpy ).toHaveBeenCalledWith( 'A' );
} );
await waitFor( () => {
expect( queryAllByRole( 'option' ) ).toHaveLength( 3 );
@@ -119,7 +119,7 @@ describe( 'Search', () => {
userEvent.type( getByRole( 'combobox' ), 'A' );
// Wait for async options processing.
await waitFor( () => {
- expect( optionsSpy ).toBeCalledWith( 'A' );
+ expect( optionsSpy ).toHaveBeenCalledWith( 'A' );
} );
await waitFor( () => {
expect( queryAllByRole( 'option' ) ).toHaveLength( 3 );
diff --git a/packages/js/components/src/tag/index.tsx b/packages/js/components/src/tag/index.tsx
index 2da5a6f7aef..d7e62f36ad9 100644
--- a/packages/js/components/src/tag/index.tsx
+++ b/packages/js/components/src/tag/index.tsx
@@ -2,16 +2,20 @@
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
-import { createElement, Fragment, useState } from '@wordpress/element';
+import {
+ createElement,
+ forwardRef,
+ Fragment,
+ useState,
+} from '@wordpress/element';
import classnames from 'classnames';
import { Button, Popover } from '@wordpress/components';
import { Icon, closeSmall } from '@wordpress/icons';
import { decodeEntities } from '@wordpress/html-entities';
-import { withInstanceId } from '@wordpress/compose';
+import { Ref } from 'react';
+import { useInstanceId } from '@wordpress/compose';
type Props = {
- /** A unique ID for this instance of the component. This is automatically generated by withInstanceId. */
- instanceId: number | string;
/** The name for this item, displayed as the tag's text. */
label: string;
/** A unique ID for this item. This is used to identify the item when the remove button is clicked. */
@@ -28,72 +32,84 @@ type Props = {
className?: string;
};
-const Tag: React.VFC< Props > = ( {
- id,
- instanceId,
- label,
- popoverContents,
- remove,
- screenReaderLabel,
- className,
-} ) => {
- const [ isVisible, setIsVisible ] = useState( false );
+const Tag = forwardRef(
+ (
+ {
+ id,
+ label,
+ popoverContents,
+ remove,
+ screenReaderLabel,
+ className,
+ }: Props,
+ removeButtonRef: Ref< HTMLButtonElement >
+ ) => {
+ const [ isVisible, setIsVisible ] = useState( false );
- screenReaderLabel = screenReaderLabel || label;
- if ( ! label ) {
- // A null label probably means something went wrong
- // @todo Maybe this should be a loading indicator?
- return null;
- }
- label = decodeEntities( label );
- const classes = classnames( 'woocommerce-tag', className, {
- 'has-remove': !! remove,
- } );
- const labelId = `woocommerce-tag__label-${ instanceId }`;
- const labelTextNode = (
-
- { screenReaderLabel }
- { label }
-
- );
+ const instanceId = useInstanceId( Tag ) as string;
- return (
-
- { popoverContents ? (
- setIsVisible( true ) }
- >
- { labelTextNode }
-
- ) : (
-
- { labelTextNode }
+ screenReaderLabel = screenReaderLabel || label;
+ if ( ! label ) {
+ // A null label probably means something went wrong
+ // @todo Maybe this should be a loading indicator?
+ return null;
+ }
+ label = decodeEntities( label );
+ const classes = classnames( 'woocommerce-tag', className, {
+ 'has-remove': !! remove,
+ } );
+ const labelId = `woocommerce-tag__label-${ instanceId }`;
+ const labelTextNode = (
+
+
+ { screenReaderLabel }
- ) }
- { popoverContents && isVisible && (
- setIsVisible( false ) }>
- { popoverContents }
-
- ) }
- { remove && (
-
-
-
- ) }
-
- );
-};
+ { label }
+
+ );
-export default withInstanceId( Tag );
+ return (
+
+ { popoverContents ? (
+ setIsVisible( true ) }
+ >
+ { labelTextNode }
+
+ ) : (
+
+ { labelTextNode }
+
+ ) }
+ { popoverContents && isVisible && (
+ setIsVisible( false ) }>
+ { popoverContents }
+
+ ) }
+ { remove && (
+
+
+
+ ) }
+
+ );
+ }
+);
+
+export default Tag;
diff --git a/packages/js/data/changelog/47614-tweak-google-extension-rename b/packages/js/data/changelog/47614-tweak-google-extension-rename
new file mode 100644
index 00000000000..4ac5ec3e8d9
--- /dev/null
+++ b/packages/js/data/changelog/47614-tweak-google-extension-rename
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Rename Google Listings and Ads with Google for WooCommerce #### Comment
\ No newline at end of file
diff --git a/packages/js/data/changelog/fix-misspelling-in-packages-js-resolvers-error-msgs b/packages/js/data/changelog/fix-misspelling-in-packages-js-resolvers-error-msgs
new file mode 100644
index 00000000000..29dbb025b1f
--- /dev/null
+++ b/packages/js/data/changelog/fix-misspelling-in-packages-js-resolvers-error-msgs
@@ -0,0 +1,4 @@
+Significance: minor
+Type: tweak
+
+Fix typos in resolvers error message
diff --git a/packages/js/data/changelog/tweak-google-extension-rename b/packages/js/data/changelog/tweak-google-extension-rename
new file mode 100644
index 00000000000..8a85377cbb9
--- /dev/null
+++ b/packages/js/data/changelog/tweak-google-extension-rename
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Rename Google Listings and Ads with Google for WooCommerce
diff --git a/packages/js/data/src/plugins/constants.ts b/packages/js/data/src/plugins/constants.ts
index 2fe65fad645..5c5479927a9 100644
--- a/packages/js/data/src/plugins/constants.ts
+++ b/packages/js/data/src/plugins/constants.ts
@@ -52,7 +52,7 @@ export const pluginNames = {
'Mercado Pago payments for WooCommerce',
'woocommerce'
),
- 'google-listings-and-ads': __( 'Google Listings and Ads', 'woocommerce' ),
+ 'google-listings-and-ads': __( 'Google for WooCommerce', 'woocommerce' ),
'woo-razorpay': __( 'Razorpay', 'woocommerce' ),
mailpoet: __( 'MailPoet', 'woocommerce' ),
'pinterest-for-woocommerce': __(
diff --git a/packages/js/data/src/reports/resolvers.ts b/packages/js/data/src/reports/resolvers.ts
index 373916737d8..2035ad9e307 100644
--- a/packages/js/data/src/reports/resolvers.ts
+++ b/packages/js/data/src/reports/resolvers.ts
@@ -35,7 +35,7 @@ const getIntHeaderValues = (
const value = response.headers.get( key );
if ( value === undefined ) {
throw new Error(
- `Malformed response from server. '${ key }' header is missing when retriving ./report/${ endpoint }.`
+ `Malformed response from server. '${ key }' header is missing when retrieving ./report/${ endpoint }.`
);
}
return parseInt( value, 10 );
diff --git a/packages/js/data/src/reviews/resolvers.ts b/packages/js/data/src/reviews/resolvers.ts
index 98f23bfbd41..22fa2870e4c 100644
--- a/packages/js/data/src/reviews/resolvers.ts
+++ b/packages/js/data/src/reviews/resolvers.ts
@@ -26,7 +26,7 @@ export function* getReviews( query: ReviewsQueryParams ) {
if ( totalCountFromHeader === undefined ) {
throw new Error(
- "Malformed response from server. 'x-wp-total' header is missing when retriving ./products/reviews."
+ "Malformed response from server. 'x-wp-total' header is missing when retrieving ./products/reviews."
);
}
const totalCount = parseInt( totalCountFromHeader, 10 );
diff --git a/packages/js/experimental/CHANGELOG.md b/packages/js/experimental/CHANGELOG.md
index 50de636bc67..e5ee51e40ef 100644
--- a/packages/js/experimental/CHANGELOG.md
+++ b/packages/js/experimental/CHANGELOG.md
@@ -2,11 +2,48 @@
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [3.2.0](https://www.npmjs.com/package/@woocommerce/packages/js/experimental/v/3.2.0) - 2022-07-08
+## [3.3.0](https://www.npmjs.com/package/@woocommerce/experimental/v/3.3.0) - 2024-07-25
+
+- Patch - Added in missing TS definitions in package.json [#34154]
+- Patch - Check for note actions before checking length [#35396]
+- Patch - Corrected build configuration for packages that weren't outputting minified code. [#43716]
+- Patch - Fix invalid return callback ref warning [#37655]
+- Patch - Fix Launch Your Store task item should not be clickable once completed [#46361]
+- Patch - Fix missing fills prop in useSlotFills return object for wp.components >= 21.2.0 [#36887]
+- Patch - Fix remote inbox layout overflows the page width [#47451]
+- Minor - Bump node version. [#45148]
+- Patch - bump php version in packages/js/*/composer.json [#42020]
+- Patch - Support direction prop to control which direction hidden items open. [#36806]
+- Patch - update references to woocommerce.com to now reference woo.com [#41241]
+- Patch - Update TaskItem to include a badge next to the title. Update also related components TaskList and SetupTaskList, as well as docs, storybook, and tests. [#40034]
+- Patch - Update Woo.com references to WooCommerce.com. [#46259]
+- Patch - Add missing type definitions and add babel config for tests [#34428]
+- Minor - Adjust build/test scripts to remove -- -- that was required for pnpm 6. [#34661]
+- Minor - Fix lint issues [#36988]
+- Minor - Fix node and pnpm versions via engines [#34773]
+- Minor - Improve the "Dismiss" button visibility [#35060]
+- Patch - Make eslint emit JSON report for annotating PRs. [#39704]
+- Minor - Match TypeScript version with syncpack [#34787]
+- Patch - Merging trunk with local [#34322]
+- Minor - Sync @wordpress package versions via syncpack. [#37034]
+- Patch - Update dependencies [#48645]
+- Patch - Update eslint to 8.32.0 across the monorepo. [#36700]
+- Patch - Update events that should trigger the test job(s) [#47612]
+- Minor - Update pnpm monorepo-wide to 8.6.5 [#38990]
+- Minor - Update pnpm to 8.6.7 [#39245]
+- Patch - Update pnpm to 9.1.0 [#47385]
+- Minor - Update pnpm to version 8. [#37915]
+- Minor - Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues [#35007]
+- Patch - Update webpack config to use @woocommerce/internal-style-build's parser config [#37195]
+- Patch - Upgraded Storybook to 6.5.17-alpha.0 for TypeScript 5 compatibility [#39745]
+- Minor - Upgrade TypeScript to 5.1.6 [#39531]
+- Patch - Correct spelling errors [#37887]
+
+## [3.2.0](https://www.npmjs.com/package/@woocommerce/experimental/v/3.2.0) - 2022-07-08
- Minor - Remove PHP and Composer dependencies for packaged JS packages
-## [3.1.0](https://www.npmjs.com/package/@woocommerce/packages/js/experimental/v/3.1.0) - 2022-06-14
+## [3.1.0](https://www.npmjs.com/package/@woocommerce/experimental/v/3.1.0) - 2022-06-14
- Minor - Add Jetpack Changelogger
- Minor - Update TaskItem props type definition.
diff --git a/packages/js/experimental/changelog/41498-dev-make-lint-output-console b/packages/js/experimental/changelog/41498-dev-make-lint-output-console
deleted file mode 100644
index 14b7d0db64b..00000000000
--- a/packages/js/experimental/changelog/41498-dev-make-lint-output-console
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Just changing package.json command for lint
-
diff --git a/packages/js/experimental/changelog/42802-fix-watch-build-race-condition b/packages/js/experimental/changelog/42802-fix-watch-build-race-condition
deleted file mode 100644
index 28192460e08..00000000000
--- a/packages/js/experimental/changelog/42802-fix-watch-build-race-condition
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Only a change to development tooling.
-
diff --git a/packages/js/experimental/changelog/43532-add-cli-to-ci-workflow b/packages/js/experimental/changelog/43532-add-cli-to-ci-workflow
deleted file mode 100644
index 060cf397930..00000000000
--- a/packages/js/experimental/changelog/43532-add-cli-to-ci-workflow
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: This is a CI-only change.
-
diff --git a/packages/js/experimental/changelog/43595-update-wireit-0.14.3 b/packages/js/experimental/changelog/43595-update-wireit-0.14.3
deleted file mode 100644
index bc75313533d..00000000000
--- a/packages/js/experimental/changelog/43595-update-wireit-0.14.3
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: This is a developer-only build tooling related change.
-
diff --git a/packages/js/experimental/changelog/43716-fix-production-builds b/packages/js/experimental/changelog/43716-fix-production-builds
deleted file mode 100644
index 8ed5422a222..00000000000
--- a/packages/js/experimental/changelog/43716-fix-production-builds
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Corrected build configuration for packages that weren't outputting minified code.
\ No newline at end of file
diff --git a/packages/js/experimental/changelog/46278-fix-43889-43901-43944 b/packages/js/experimental/changelog/46278-fix-43889-43901-43944
deleted file mode 100644
index 021f0de1698..00000000000
--- a/packages/js/experimental/changelog/46278-fix-43889-43901-43944
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: adds `glob`, `rimraf`, and `uuid` to Syncpack
-
diff --git a/packages/js/experimental/changelog/46361-fix-lys-task-not-clickable-once-completed b/packages/js/experimental/changelog/46361-fix-lys-task-not-clickable-once-completed
deleted file mode 100644
index d0ba8df72ba..00000000000
--- a/packages/js/experimental/changelog/46361-fix-lys-task-not-clickable-once-completed
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fix Launch Your Store task item should not be clickable once completed
\ No newline at end of file
diff --git a/packages/js/experimental/changelog/47207-dev-fix-experimental-tsconfig b/packages/js/experimental/changelog/47207-dev-fix-experimental-tsconfig
deleted file mode 100644
index 6bcc3eb4c14..00000000000
--- a/packages/js/experimental/changelog/47207-dev-fix-experimental-tsconfig
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Fix a persistent build bug where TS would try compile files outside of src and typings in packages/js/experimental
-
diff --git a/packages/js/experimental/changelog/bump-js-packages-php-version b/packages/js/experimental/changelog/bump-js-packages-php-version
deleted file mode 100644
index de04718dfb6..00000000000
--- a/packages/js/experimental/changelog/bump-js-packages-php-version
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-bump php version in packages/js/*/composer.json
diff --git a/packages/js/experimental/changelog/ci-add-workflow-call-event b/packages/js/experimental/changelog/ci-add-workflow-call-event
deleted file mode 100644
index 4a94d942fe9..00000000000
--- a/packages/js/experimental/changelog/ci-add-workflow-call-event
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Update events that should trigger the test job(s)
diff --git a/packages/js/experimental/changelog/dev-34928_improve_dismiss_button_visibility b/packages/js/experimental/changelog/dev-34928_improve_dismiss_button_visibility
deleted file mode 100644
index b76b3d295b8..00000000000
--- a/packages/js/experimental/changelog/dev-34928_improve_dismiss_button_visibility
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-Improve the "Dismiss" button visibility
diff --git a/packages/js/experimental/changelog/dev-bump-pnpm-version-restraint b/packages/js/experimental/changelog/dev-bump-pnpm-version-restraint
deleted file mode 100644
index f7511cb6974..00000000000
--- a/packages/js/experimental/changelog/dev-bump-pnpm-version-restraint
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-Update pnpm version constraint to 7.13.3 to avoid auto-install-peers issues
diff --git a/packages/js/experimental/changelog/dev-consolidate-eslint-versions b/packages/js/experimental/changelog/dev-consolidate-eslint-versions
deleted file mode 100644
index d3d95c39119..00000000000
--- a/packages/js/experimental/changelog/dev-consolidate-eslint-versions
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Update eslint to 8.32.0 across the monorepo.
diff --git a/packages/js/experimental/changelog/dev-fix-admin-tests-pnpm7 b/packages/js/experimental/changelog/dev-fix-admin-tests-pnpm7
deleted file mode 100644
index d8b487150a2..00000000000
--- a/packages/js/experimental/changelog/dev-fix-admin-tests-pnpm7
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Add missing type definitions and add babel config for tests
diff --git a/packages/js/experimental/changelog/dev-fix-pnpm-version-engines b/packages/js/experimental/changelog/dev-fix-pnpm-version-engines
deleted file mode 100644
index a1804a282f0..00000000000
--- a/packages/js/experimental/changelog/dev-fix-pnpm-version-engines
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-Fix node and pnpm versions via engines
diff --git a/packages/js/experimental/changelog/dev-include-eslint-annotations b/packages/js/experimental/changelog/dev-include-eslint-annotations
deleted file mode 100644
index bded8d77ece..00000000000
--- a/packages/js/experimental/changelog/dev-include-eslint-annotations
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Make eslint emit JSON report for annotating PRs.
diff --git a/packages/js/experimental/changelog/dev-pin-wp-deps-6 b/packages/js/experimental/changelog/dev-pin-wp-deps-6
deleted file mode 100644
index 551e0919dac..00000000000
--- a/packages/js/experimental/changelog/dev-pin-wp-deps-6
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-Sync @wordpress package versions via syncpack.
diff --git a/packages/js/experimental/changelog/dev-simplify-turbo b/packages/js/experimental/changelog/dev-simplify-turbo
deleted file mode 100644
index 0d230384010..00000000000
--- a/packages/js/experimental/changelog/dev-simplify-turbo
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Package scripts were modified to support simplified running of turbo commands in the monorepo.
-
-
diff --git a/packages/js/experimental/changelog/dev-sync-pnpm b/packages/js/experimental/changelog/dev-sync-pnpm
deleted file mode 100644
index 2e1c2db5705..00000000000
--- a/packages/js/experimental/changelog/dev-sync-pnpm
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-Update pnpm to 8.6.7
diff --git a/packages/js/experimental/changelog/dev-update-eslint-plugin b/packages/js/experimental/changelog/dev-update-eslint-plugin
deleted file mode 100644
index 47894ec9c6c..00000000000
--- a/packages/js/experimental/changelog/dev-update-eslint-plugin
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-Fix lint issues
diff --git a/packages/js/experimental/changelog/dev-update-pnpm-8.6.5 b/packages/js/experimental/changelog/dev-update-pnpm-8.6.5
deleted file mode 100644
index 601ddf2c1c8..00000000000
--- a/packages/js/experimental/changelog/dev-update-pnpm-8.6.5
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-Update pnpm monorepo-wide to 8.6.5
diff --git a/packages/js/experimental/changelog/dev-update-pnpm8 b/packages/js/experimental/changelog/dev-update-pnpm8
deleted file mode 100644
index c21e013f454..00000000000
--- a/packages/js/experimental/changelog/dev-update-pnpm8
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-Update pnpm to version 8.
diff --git a/packages/js/experimental/changelog/dev-upgrade-storybook-6-5-17 b/packages/js/experimental/changelog/dev-upgrade-storybook-6-5-17
deleted file mode 100644
index 2a77f7ef118..00000000000
--- a/packages/js/experimental/changelog/dev-upgrade-storybook-6-5-17
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Upgraded Storybook to 6.5.17-alpha.0 for TypeScript 5 compatibility
diff --git a/packages/js/experimental/changelog/fix-34112 b/packages/js/experimental/changelog/fix-34112
deleted file mode 100644
index 2c06f8f49fb..00000000000
--- a/packages/js/experimental/changelog/fix-34112
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: Minor update of react and react-dom to 17.0.2.
-
-
diff --git a/packages/js/experimental/changelog/fix-34929 b/packages/js/experimental/changelog/fix-34929
deleted file mode 100644
index 2fedcc1e932..00000000000
--- a/packages/js/experimental/changelog/fix-34929
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Check for note actions before checking length
diff --git a/packages/js/experimental/changelog/fix-37654-invalid-return-callback-ref b/packages/js/experimental/changelog/fix-37654-invalid-return-callback-ref
deleted file mode 100644
index d14e18f1448..00000000000
--- a/packages/js/experimental/changelog/fix-37654-invalid-return-callback-ref
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fix invalid return callback ref warning
diff --git a/packages/js/experimental/changelog/fix-45516-remote-inbox-layout-overflow b/packages/js/experimental/changelog/fix-45516-remote-inbox-layout-overflow
deleted file mode 100644
index 17035a86575..00000000000
--- a/packages/js/experimental/changelog/fix-45516-remote-inbox-layout-overflow
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fix remote inbox layout overflows the page width
diff --git a/packages/js/experimental/changelog/fix-missing-type-definitions-dependencies b/packages/js/experimental/changelog/fix-missing-type-definitions-dependencies
deleted file mode 100644
index 06ebf8aa257..00000000000
--- a/packages/js/experimental/changelog/fix-missing-type-definitions-dependencies
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Added in missing TS definitions in package.json
\ No newline at end of file
diff --git a/packages/js/experimental/changelog/fix-plugin-installer-ts b/packages/js/experimental/changelog/fix-plugin-installer-ts
deleted file mode 100644
index 77d0a173670..00000000000
--- a/packages/js/experimental/changelog/fix-plugin-installer-ts
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-Match TypeScript version with syncpack
diff --git a/packages/js/experimental/changelog/fix-slot-fills-for-wp-6_2 b/packages/js/experimental/changelog/fix-slot-fills-for-wp-6_2
deleted file mode 100644
index 429871a1cc1..00000000000
--- a/packages/js/experimental/changelog/fix-slot-fills-for-wp-6_2
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fix missing fills prop in useSlotFills return object for wp.components >= 21.2.0
diff --git a/packages/js/experimental/changelog/fix-typescript-incremental-builds b/packages/js/experimental/changelog/fix-typescript-incremental-builds
deleted file mode 100644
index f2bdc9a96ae..00000000000
--- a/packages/js/experimental/changelog/fix-typescript-incremental-builds
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: dev
-Comment: TypeScript build change
-
-
diff --git a/packages/js/experimental/changelog/fix-typescript-package-isolation b/packages/js/experimental/changelog/fix-typescript-package-isolation
deleted file mode 100644
index 2d087939231..00000000000
--- a/packages/js/experimental/changelog/fix-typescript-package-isolation
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Configuration change only
-
-
diff --git a/packages/js/experimental/changelog/update-36805-support-direction-prop b/packages/js/experimental/changelog/update-36805-support-direction-prop
deleted file mode 100644
index 22cfe4b80ad..00000000000
--- a/packages/js/experimental/changelog/update-36805-support-direction-prop
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: update
-
-Support direction prop to control which direction hidden items open.
-
diff --git a/packages/js/experimental/changelog/update-changelogger b/packages/js/experimental/changelog/update-changelogger
deleted file mode 100644
index 1674c919e78..00000000000
--- a/packages/js/experimental/changelog/update-changelogger
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: fix
-Comment: Dev dependency update.
-
-
diff --git a/packages/js/experimental/changelog/update-separate-php-and-js-tests b/packages/js/experimental/changelog/update-separate-php-and-js-tests
deleted file mode 100644
index 12fd177d1fa..00000000000
--- a/packages/js/experimental/changelog/update-separate-php-and-js-tests
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: This is just a change to developer commands.
-
diff --git a/packages/js/experimental/changelog/update-task-list-item-and-woopayments-task b/packages/js/experimental/changelog/update-task-list-item-and-woopayments-task
deleted file mode 100644
index 845b7792622..00000000000
--- a/packages/js/experimental/changelog/update-task-list-item-and-woopayments-task
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Update TaskItem to include a badge next to the title. Update also related components TaskList and SetupTaskList, as well as docs, storybook, and tests.
diff --git a/packages/js/experimental/changelog/update-webpack-invalid-export-error b/packages/js/experimental/changelog/update-webpack-invalid-export-error
deleted file mode 100644
index d844dbd186f..00000000000
--- a/packages/js/experimental/changelog/update-webpack-invalid-export-error
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Update webpack config to use @woocommerce/internal-style-build's parser config
diff --git a/packages/js/experimental/changelog/update-woo-com-to-woocommerce-com b/packages/js/experimental/changelog/update-woo-com-to-woocommerce-com
deleted file mode 100644
index 36ca65dd012..00000000000
--- a/packages/js/experimental/changelog/update-woo-com-to-woocommerce-com
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Update Woo.com references to WooCommerce.com.
diff --git a/packages/js/experimental/changelog/update-woocommerce-com-to-woo-com b/packages/js/experimental/changelog/update-woocommerce-com-to-woo-com
deleted file mode 100644
index 21303d2dbba..00000000000
--- a/packages/js/experimental/changelog/update-woocommerce-com-to-woo-com
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-update references to woocommerce.com to now reference woo.com
diff --git a/packages/js/experimental/changelog/upgrade-pnpm-7 b/packages/js/experimental/changelog/upgrade-pnpm-7
deleted file mode 100644
index 10ee28d636f..00000000000
--- a/packages/js/experimental/changelog/upgrade-pnpm-7
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-Adjust build/test scripts to remove -- -- that was required for pnpm 6.
diff --git a/packages/js/experimental/package.json b/packages/js/experimental/package.json
index 7fbebf82079..de525dbc624 100644
--- a/packages/js/experimental/package.json
+++ b/packages/js/experimental/package.json
@@ -1,6 +1,6 @@
{
"name": "@woocommerce/experimental",
- "version": "3.2.0",
+ "version": "3.3.0",
"description": "WooCommerce experimental components.",
"author": "Automattic",
"license": "GPL-3.0-or-later",
diff --git a/packages/js/explat/changelog/dev-monorepo-caching-deps-per-target-package b/packages/js/explat/changelog/dev-monorepo-caching-deps-per-target-package
new file mode 100644
index 00000000000..7a52e719aa5
--- /dev/null
+++ b/packages/js/explat/changelog/dev-monorepo-caching-deps-per-target-package
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+CI: code style fixes to pass linting in updated CI environment.
diff --git a/packages/js/explat/src/test/assignment-test.js b/packages/js/explat/src/test/assignment-test.js
index 860f671592c..ce70a46c093 100644
--- a/packages/js/explat/src/test/assignment-test.js
+++ b/packages/js/explat/src/test/assignment-test.js
@@ -50,7 +50,7 @@ describe( 'fetchExperimentAssignment', () => {
experimentName: '123',
anonId: null,
} );
- await expect( fetchPromise ).rejects.toThrowError();
+ await expect( fetchPromise ).rejects.toThrow();
} );
it( 'should throw error when experiment_name is empty', async () => {
@@ -58,7 +58,7 @@ describe( 'fetchExperimentAssignment', () => {
experimentName: '',
anonId: null,
} );
- await expect( fetchPromise ).rejects.toThrowError();
+ await expect( fetchPromise ).rejects.toThrow();
} );
it( 'should throw error when experiment_name is invalid', async () => {
@@ -66,7 +66,7 @@ describe( 'fetchExperimentAssignment', () => {
experimentName: '',
anonId: null,
} );
- await expect( fetchPromise ).rejects.toThrowError();
+ await expect( fetchPromise ).rejects.toThrow();
} );
it( 'should return .json response', async () => {
diff --git a/packages/js/extend-cart-checkout-block/$slug.php.mustache b/packages/js/extend-cart-checkout-block/$slug.php.mustache
index a1308c44c8d..1a4c7ba296b 100644
--- a/packages/js/extend-cart-checkout-block/$slug.php.mustache
+++ b/packages/js/extend-cart-checkout-block/$slug.php.mustache
@@ -20,6 +20,7 @@
*/
add_action( 'woocommerce_blocks_loaded',
function () {
+ register_block_type_from_metadata( __DIR__ . '/build/js/checkout-newsletter-subscription-block' );
require_once __DIR__ . '/{{slug}}-blocks-integration.php';
add_action(
'woocommerce_blocks_cart_block_registration',
diff --git a/packages/js/extend-cart-checkout-block/CHANGELOG.md b/packages/js/extend-cart-checkout-block/CHANGELOG.md
index 55dbd4a811b..fd99dfee926 100644
--- a/packages/js/extend-cart-checkout-block/CHANGELOG.md
+++ b/packages/js/extend-cart-checkout-block/CHANGELOG.md
@@ -2,13 +2,17 @@
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.3.1](https://www.npmjs.com/package/@woocommerce/extend-cart-checkout-block/v/1.3.1) - 2024-07-10
+
+- Patch - Ensures the default text shows when saving the example newsletter subscription block. [#48581]
+
## [1.3.0](https://www.npmjs.com/package/@woocommerce/extend-cart-checkout-block/v/1.3.0) - 2024-06-14
- Minor - Adds an example of using the Additional Checkout Fields API [#48280]
- Minor - Bump node version. [#45148]
-- Patch - bump php version in packages/js/*/composer.json [#42020]
- Minor - Update pnpm monorepo-wide to 8.6.5 [#38990]
- Minor - Update pnpm to 8.6.7 [#39245]
+- Patch - bump php version in packages/js/*/composer.json [#42020]
- Patch - Update pnpm to 9.1.0 [#47385]
## [1.2.0](https://www.npmjs.com/package/@woocommerce/extend-cart-checkout-block/v/1.2.0) - 2023-04-26
diff --git a/packages/js/extend-cart-checkout-block/package.json b/packages/js/extend-cart-checkout-block/package.json
index 024f3e46a0c..5695910fd8c 100644
--- a/packages/js/extend-cart-checkout-block/package.json
+++ b/packages/js/extend-cart-checkout-block/package.json
@@ -1,6 +1,6 @@
{
"name": "@woocommerce/extend-cart-checkout-block",
- "version": "1.3.0",
+ "version": "1.3.1",
"description": "",
"main": "index.js",
"engines": {
diff --git a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/edit.js.mustache b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/edit.js.mustache
index f55abeb2b72..094897462c5 100644
--- a/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/edit.js.mustache
+++ b/packages/js/extend-cart-checkout-block/src/js/checkout-newsletter-subscription-block/edit.js.mustache
@@ -43,7 +43,7 @@ export const Save = ( { attributes } ) => {
const { text } = attributes;
return (
-
+
);
-};
\ No newline at end of file
+};
diff --git a/packages/js/experimental/changelog/update-jest-merge b/packages/js/internal-js-tests/changelog/dev-babel-loader-jest-caching
similarity index 50%
rename from packages/js/experimental/changelog/update-jest-merge
rename to packages/js/internal-js-tests/changelog/dev-babel-loader-jest-caching
index 3ffb0e90b2a..49889afff8e 100644
--- a/packages/js/experimental/changelog/update-jest-merge
+++ b/packages/js/internal-js-tests/changelog/dev-babel-loader-jest-caching
@@ -1,4 +1,4 @@
Significance: patch
Type: dev
-Merging trunk with local
+Monorepo: enable Jest caching.
diff --git a/packages/js/internal-js-tests/changelog/dev-monorepo-caching-deps-per-target-package b/packages/js/internal-js-tests/changelog/dev-monorepo-caching-deps-per-target-package
new file mode 100644
index 00000000000..7a52e719aa5
--- /dev/null
+++ b/packages/js/internal-js-tests/changelog/dev-monorepo-caching-deps-per-target-package
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+CI: code style fixes to pass linting in updated CI environment.
diff --git a/packages/js/internal-js-tests/jest-preset.js b/packages/js/internal-js-tests/jest-preset.js
index 451e1373986..f3ad011aaab 100644
--- a/packages/js/internal-js-tests/jest-preset.js
+++ b/packages/js/internal-js-tests/jest-preset.js
@@ -5,7 +5,7 @@ const path = require( 'path' );
// These modules need to be transformed because they are not transpiled to CommonJS.
// The top-level keys are the names of the packages and the values are the file
-// regexes that need to be transformed. Note that these are relative to the
+// regexes that need to be transformed. Note that these are relative to the
// package root and should be treated as such.
const transformModules = {
'is-plain-obj': {
@@ -76,4 +76,8 @@ module.exports = {
testEnvironment: 'jest-environment-jsdom',
timers: 'modern',
verbose: true,
+ cacheDirectory: path.resolve(
+ __dirname,
+ '../../../node_modules/.cache/jest'
+ ),
};
diff --git a/packages/js/internal-js-tests/src/setup-globals.js b/packages/js/internal-js-tests/src/setup-globals.js
index e2b1469fa8a..68686994952 100644
--- a/packages/js/internal-js-tests/src/setup-globals.js
+++ b/packages/js/internal-js-tests/src/setup-globals.js
@@ -5,10 +5,6 @@ import { setLocaleData } from '@wordpress/i18n';
import { registerStore } from '@wordpress/data';
import 'regenerator-runtime/runtime';
-// Mock the config module to avoid errors like:
-// Core Error: Could not find config value for key ${ key }. Please make sure that if you need it then it has a default value assigned in config/_shared.json.
-jest.mock( '@automattic/calypso-config' );
-
// Due to the dependency @wordpress/compose which introduces the use of
// ResizeObserver this global mock is required for some tests to work.
global.ResizeObserver = require( 'resize-observer-polyfill' );
diff --git a/packages/js/internal-style-build/index.js b/packages/js/internal-style-build/index.js
index e7d93fc8431..ccf4b5eda23 100644
--- a/packages/js/internal-style-build/index.js
+++ b/packages/js/internal-style-build/index.js
@@ -21,7 +21,11 @@ module.exports = {
rules: [
{
test: /\.s?css$/,
- exclude: [ /storybook\/wordpress/, /build-style\/*\/*.css/ ],
+ exclude: [
+ /storybook\/wordpress/,
+ /build-style\/*\/*.css/,
+ /[\/\\](changelog|bin|docs|build|build-module|build-types|vendor|tests|test)[\/\\]/,
+ ],
use: [
MiniCssExtractPlugin.loader,
'css-loader',
diff --git a/packages/js/navigation/changelog/dev-monorepo-caching-deps-per-target-package b/packages/js/navigation/changelog/dev-monorepo-caching-deps-per-target-package
new file mode 100644
index 00000000000..7a52e719aa5
--- /dev/null
+++ b/packages/js/navigation/changelog/dev-monorepo-caching-deps-per-target-package
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+CI: code style fixes to pass linting in updated CI environment.
diff --git a/packages/js/navigation/src/test/index.js b/packages/js/navigation/src/test/index.js
index b0c705e560c..3477dfb8063 100644
--- a/packages/js/navigation/src/test/index.js
+++ b/packages/js/navigation/src/test/index.js
@@ -335,7 +335,7 @@ describe( 'navigateTo', () => {
const resultUrl = new URL( window.location.href );
- expect( getHistory().push ).not.toBeCalled();
+ expect( getHistory().push ).not.toHaveBeenCalled();
expect( resultUrl.search ).toBe(
'?page=wc-admin&path=%2Fsetup-wizard'
);
@@ -353,7 +353,7 @@ describe( 'navigateTo', () => {
const resultUrl = new URL( window.location.href );
- expect( getHistory().push ).not.toBeCalled();
+ expect( getHistory().push ).not.toHaveBeenCalled();
expect( resultUrl.toString() ).toBe(
'https://vagrant.local/wp/wp-admin/orders.php'
);
@@ -385,7 +385,7 @@ describe( 'navigateTo', () => {
const resultUrl = new URL( window.location.href );
- expect( getHistory().push ).not.toBeCalled();
+ expect( getHistory().push ).not.toHaveBeenCalled();
expect( resultUrl.toString() ).toBe(
'https://vagrant.local/wp/wp-admin/admin.php?page=wc-admin&path=%2Fsetup-wizard'
);
diff --git a/packages/js/experimental/changelog/42860-update-pnpm b/packages/js/notices/changelog/dev-updates-to-packages-for-dependencies
similarity index 58%
rename from packages/js/experimental/changelog/42860-update-pnpm
rename to packages/js/notices/changelog/dev-updates-to-packages-for-dependencies
index 69a57db741b..213a23df05a 100644
--- a/packages/js/experimental/changelog/42860-update-pnpm
+++ b/packages/js/notices/changelog/dev-updates-to-packages-for-dependencies
@@ -1,4 +1,4 @@
Significance: patch
Type: dev
-Comment: Updated PNPM
+Update dependencies
diff --git a/packages/js/onboarding/CHANGELOG.md b/packages/js/onboarding/CHANGELOG.md
index 20ed7747730..f13b341cb80 100644
--- a/packages/js/onboarding/CHANGELOG.md
+++ b/packages/js/onboarding/CHANGELOG.md
@@ -2,6 +2,14 @@
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [3.6.0](https://www.npmjs.com/package/@woocommerce/onboarding/v/3.6.0) - 2024-07-29
+
+- Minor - Add a new shared component to display logos of payment methods supported by WooPayments. [#49300]
+- Minor - Added Task Referral system for wc-admin onboarding tasks. [#47654]
+- Patch - Update events that should trigger the test job(s) [#47612]
+- Patch - Update pnpm to 9.1.0 [#47385]
+- Minor - Fix typo in findCountryOption test [#48648]
+
## [3.5.0](https://www.npmjs.com/package/@woocommerce/onboarding/v/3.5.0) - 2024-04-26
- Minor - Branding rollout - change WooCommerce Payments to WooPayments [#39188]
diff --git a/packages/js/onboarding/changelog/47156-dev-fix-build-error-build-style b/packages/js/onboarding/changelog/47156-dev-fix-build-error-build-style
deleted file mode 100644
index f7f4f91d551..00000000000
--- a/packages/js/onboarding/changelog/47156-dev-fix-build-error-build-style
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Fix a persistent build bug where TS would try compile files outside of src and typings directories.
-
diff --git a/packages/js/onboarding/changelog/add-task-referral-system b/packages/js/onboarding/changelog/add-task-referral-system
deleted file mode 100644
index 32411e43607..00000000000
--- a/packages/js/onboarding/changelog/add-task-referral-system
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: add
-
-Added Task Referral system for wc-admin onboarding tasks.
\ No newline at end of file
diff --git a/packages/js/onboarding/changelog/ci-add-workflow-call-event b/packages/js/onboarding/changelog/ci-add-workflow-call-event
deleted file mode 100644
index 4a94d942fe9..00000000000
--- a/packages/js/onboarding/changelog/ci-add-workflow-call-event
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Update events that should trigger the test job(s)
diff --git a/packages/js/onboarding/package.json b/packages/js/onboarding/package.json
index 638ce5dd753..55825d51629 100644
--- a/packages/js/onboarding/package.json
+++ b/packages/js/onboarding/package.json
@@ -1,6 +1,6 @@
{
"name": "@woocommerce/onboarding",
- "version": "3.5.0",
+ "version": "3.6.0",
"description": "Onboarding utilities.",
"author": "Automattic",
"license": "GPL-3.0-or-later",
diff --git a/packages/js/onboarding/src/components/WCPayBanner/Icons/amex.js b/packages/js/onboarding/src/components/WCPayBanner/Icons/amex.js
deleted file mode 100644
index 564be6610f2..00000000000
--- a/packages/js/onboarding/src/components/WCPayBanner/Icons/amex.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * External dependencies
- */
-import { createElement } from '@wordpress/element';
-
-export const Amex = () => (
- /* eslint-disable */
-
-
-
-
- /* eslint-enable */
-);
diff --git a/packages/js/onboarding/src/components/WCPayBanner/Icons/applepay.js b/packages/js/onboarding/src/components/WCPayBanner/Icons/applepay.js
deleted file mode 100644
index 198c6ea46c9..00000000000
--- a/packages/js/onboarding/src/components/WCPayBanner/Icons/applepay.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * External dependencies
- */
-import { createElement } from '@wordpress/element';
-
-export const ApplePay = () => (
- /* eslint-disable */
-
-
-
-
- /* eslint-enable */
-);
diff --git a/packages/js/onboarding/src/components/WCPayBanner/Icons/cb.js b/packages/js/onboarding/src/components/WCPayBanner/Icons/cb.js
deleted file mode 100644
index db7c1e35859..00000000000
--- a/packages/js/onboarding/src/components/WCPayBanner/Icons/cb.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * External dependencies
- */
-import { createElement } from '@wordpress/element';
-
-export const CB = () => (
- /* eslint-disable */
-
-
-
-
-
-
-
-
-
-
-
-
- /* eslint-enable */
-);
diff --git a/packages/js/onboarding/src/components/WCPayBanner/Icons/discover.js b/packages/js/onboarding/src/components/WCPayBanner/Icons/discover.js
deleted file mode 100644
index 91775302c43..00000000000
--- a/packages/js/onboarding/src/components/WCPayBanner/Icons/discover.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * External dependencies
- */
-import { createElement } from '@wordpress/element';
-
-export const Discover = () => (
- /* eslint-disable */
-
-
-
-
-
-
-
-
-
-
-
-
- /* eslint-enable */
-);
diff --git a/packages/js/onboarding/src/components/WCPayBanner/Icons/giropay.js b/packages/js/onboarding/src/components/WCPayBanner/Icons/giropay.js
deleted file mode 100644
index 63dfef83e0a..00000000000
--- a/packages/js/onboarding/src/components/WCPayBanner/Icons/giropay.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * External dependencies
- */
-import { createElement } from '@wordpress/element';
-
-export const Giropay = () => (
- /* eslint-disable */
-
-
-
-
-
-
-
-
-
-
- /* eslint-enable */
-);
diff --git a/packages/js/onboarding/src/components/WCPayBanner/Icons/gpay.js b/packages/js/onboarding/src/components/WCPayBanner/Icons/gpay.js
deleted file mode 100644
index 353c10e1e5e..00000000000
--- a/packages/js/onboarding/src/components/WCPayBanner/Icons/gpay.js
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * External dependencies
- */
-import { createElement } from '@wordpress/element';
-
-export const GooglePay = () => (
- /* eslint-disable */
-
-
-
-
-
-
-
-
-
-
- /* eslint-enable */
-);
diff --git a/packages/js/onboarding/src/components/WCPayBanner/Icons/index.js b/packages/js/onboarding/src/components/WCPayBanner/Icons/index.js
deleted file mode 100644
index b8be1a2aa5c..00000000000
--- a/packages/js/onboarding/src/components/WCPayBanner/Icons/index.js
+++ /dev/null
@@ -1,12 +0,0 @@
-export * from './amex';
-export * from './applepay';
-export * from './cb';
-export * from './discover';
-export * from './giropay';
-export * from './gpay';
-export * from './jcb';
-export * from './maestro';
-export * from './mastercard';
-export * from './unionpay';
-export * from './visa';
-export * from './woopay';
diff --git a/packages/js/onboarding/src/components/WCPayBanner/Icons/jcb.js b/packages/js/onboarding/src/components/WCPayBanner/Icons/jcb.js
deleted file mode 100644
index e817776d17f..00000000000
--- a/packages/js/onboarding/src/components/WCPayBanner/Icons/jcb.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * External dependencies
- */
-import { createElement } from '@wordpress/element';
-
-export const JCB = () => (
- /* eslint-disable */
-
-
-
-
-
-
- /* eslint-enable */
-);
diff --git a/packages/js/onboarding/src/components/WCPayBanner/Icons/maestro.js b/packages/js/onboarding/src/components/WCPayBanner/Icons/maestro.js
deleted file mode 100644
index b877a289687..00000000000
--- a/packages/js/onboarding/src/components/WCPayBanner/Icons/maestro.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * External dependencies
- */
-import { createElement } from '@wordpress/element';
-
-export const Maestro = () => (
- /* eslint-disable */
-
-
-
-
-
-
-
-
-
-
-
-
-
- /* eslint-enable */
-);
diff --git a/packages/js/onboarding/src/components/WCPayBanner/Icons/mastercard.js b/packages/js/onboarding/src/components/WCPayBanner/Icons/mastercard.js
deleted file mode 100644
index 8a4d0f42b6f..00000000000
--- a/packages/js/onboarding/src/components/WCPayBanner/Icons/mastercard.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * External dependencies
- */
-import { createElement } from '@wordpress/element';
-
-export const MasterCard = () => (
- /* eslint-disable */
-
-
-
-
-
-
-
- /* eslint-enable */
-);
diff --git a/packages/js/onboarding/src/components/WCPayBanner/Icons/unionpay.js b/packages/js/onboarding/src/components/WCPayBanner/Icons/unionpay.js
deleted file mode 100644
index 39c3933fafc..00000000000
--- a/packages/js/onboarding/src/components/WCPayBanner/Icons/unionpay.js
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * External dependencies
- */
-import { createElement } from '@wordpress/element';
-
-export const UnionPay = () => (
- /* eslint-disable */
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /* eslint-enable */
-);
diff --git a/packages/js/onboarding/src/components/WCPayBanner/Icons/visa.js b/packages/js/onboarding/src/components/WCPayBanner/Icons/visa.js
deleted file mode 100644
index 42370709f94..00000000000
--- a/packages/js/onboarding/src/components/WCPayBanner/Icons/visa.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * External dependencies
- */
-import { createElement } from '@wordpress/element';
-
-export const Visa = () => (
- /* eslint-disable */
-
-
-
-
-
-
-
- /* eslint-enable */
-);
diff --git a/packages/js/onboarding/src/components/WCPayBanner/PaymentMethodsIcons.tsx b/packages/js/onboarding/src/components/WCPayBanner/PaymentMethodsIcons.tsx
deleted file mode 100644
index 3a7e2db8173..00000000000
--- a/packages/js/onboarding/src/components/WCPayBanner/PaymentMethodsIcons.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * External dependencies
- */
-import { createElement } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
-import { Text } from '@woocommerce/experimental';
-
-/**
- * Internal dependencies
- */
-import {
- Visa,
- MasterCard,
- Amex,
- WooPay,
- ApplePay,
- Giropay,
- GooglePay,
- CB,
- Discover,
- UnionPay,
- JCB,
-} from './Icons';
-
-export const PaymentMethodsIcons: React.VFC< {
- isWooPayEligible: boolean;
-} > = ( { isWooPayEligible = false } ) => (
-
-
-
-
- { isWooPayEligible &&
}
-
-
-
-
-
-
-
-
- { __( '& more.', 'woocommerce' ) }
-
-
-);
diff --git a/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.scss b/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.scss
index e0d3a5d0cbf..f490a0a12e5 100644
--- a/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.scss
+++ b/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.scss
@@ -53,10 +53,13 @@
padding: 20px 15px 30px;
p {
- color: #757575;
font-style: normal;
font-weight: 400;
}
+
+ @media screen and (max-width: 1024px) {
+ justify-content: left;
+ }
}
.woocommerce-recommended-payments-banner__footer_icon_container {
diff --git a/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.tsx b/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.tsx
index 5bbb0cd7c71..b2f717cdbae 100644
--- a/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.tsx
+++ b/packages/js/onboarding/src/components/WCPayBanner/WCPayBanner.tsx
@@ -13,7 +13,7 @@ import { Text } from '@woocommerce/experimental';
/**
* Internal dependencies
*/
-import { PaymentMethodsIcons } from './PaymentMethodsIcons';
+import { WooPaymentMethodsLogos } from '../WooPaymentsMethodsLogos';
import { WCPayBannerImage } from './WCPayBannerImage';
export const WCPayBannerFooter: React.VFC< {
@@ -28,7 +28,10 @@ export const WCPayBannerFooter: React.VFC< {
) }
-
+
);
diff --git a/packages/js/onboarding/src/components/WCPayBanner/index.ts b/packages/js/onboarding/src/components/WCPayBanner/index.ts
index 452a1115322..2db0ef0a41a 100644
--- a/packages/js/onboarding/src/components/WCPayBanner/index.ts
+++ b/packages/js/onboarding/src/components/WCPayBanner/index.ts
@@ -1,5 +1,3 @@
-export * from './Icons';
export * from './WCPayBannerImage';
export * from './WCPayBannerImageCut';
-export * from './PaymentMethodsIcons';
export * from './WCPayBanner';
diff --git a/packages/js/onboarding/src/components/WooPaymentsMethodsLogos/PaymentMethodsLogos.scss b/packages/js/onboarding/src/components/WooPaymentsMethodsLogos/PaymentMethodsLogos.scss
new file mode 100644
index 00000000000..bf2f9387595
--- /dev/null
+++ b/packages/js/onboarding/src/components/WooPaymentsMethodsLogos/PaymentMethodsLogos.scss
@@ -0,0 +1,40 @@
+.woocommerce-woopayments-payment-methods-logos {
+ display: flex;
+ align-items: center;
+ padding: $gap-small 0;
+
+ svg {
+ width: 38px;
+ height: 24px;
+ margin-right: 8px;
+ border: 1px solid $gray-300;
+ border-radius: 3px;
+ }
+
+ &-count {
+ width: 38px;
+ height: 24px;
+ background-color: rgba($gray-700, 0.10);
+ color: $gray-900;
+ text-align: center;
+ line-height: 24px;
+ border-radius: 3px;
+ font-size: 11px;
+ font-weight: 600;
+ }
+}
+
+.wc_payment_gateways_wrapper [data-gateway_id="pre_install_woocommerce_payments_promotion"] {
+ .woocommerce-woopayments-payment-methods-logos {
+ padding: 0;
+ }
+
+ @media only screen and (max-width: 768px) {
+ .wc-payment-gateway-method__name {
+ display: block;
+ }
+ .pre-install-payment-gateway__subtitle {
+ margin-left: 0;
+ }
+ }
+}
diff --git a/packages/js/onboarding/src/components/WooPaymentsMethodsLogos/index.tsx b/packages/js/onboarding/src/components/WooPaymentsMethodsLogos/index.tsx
new file mode 100644
index 00000000000..355057663ff
--- /dev/null
+++ b/packages/js/onboarding/src/components/WooPaymentsMethodsLogos/index.tsx
@@ -0,0 +1,140 @@
+/**
+ * External dependencies
+ */
+import React, { useState, useEffect } from 'react';
+import { Fragment, createElement } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import Visa from '../../images/cards/visa';
+import MasterCard from '../../images/cards/mastercard';
+import Amex from '../../images/cards/amex';
+import Discover from '../../images/cards/discover';
+import ApplePay from '../../images/cards/applepay';
+import GooglePay from '../../images/cards/googlepay';
+import JCB from '../../images/cards/jcb';
+import WooPay from '../../images/payment-methods/woopay';
+import AfterPay from '../../images/payment-methods/afterpay';
+import Affirm from '../../images/payment-methods/affirm';
+import Klarna from '../../images/payment-methods/klarna';
+
+/**
+ * Payment methods logos
+ */
+const PaymentMethods = [
+ {
+ name: 'visa',
+ component: ,
+ },
+ {
+ name: 'mastercard',
+ component: ,
+ },
+ {
+ name: 'amex',
+ component: ,
+ },
+ {
+ name: 'discover',
+ component: ,
+ },
+ {
+ name: 'woopay',
+ component: ,
+ },
+ {
+ name: 'applepay',
+ component: ,
+ },
+ {
+ name: 'googlepay',
+ component: ,
+ },
+ {
+ name: 'afterpay',
+ component: ,
+ },
+ {
+ name: 'affirm',
+ component: ,
+ },
+ {
+ name: 'klarna',
+ component: ,
+ },
+ {
+ name: 'jcb',
+ component: ,
+ },
+];
+
+// Maximum number of logos to be displayed on a mobile screen.
+const maxElementsMobile = 5;
+// Maximum number of logos to be displayed on a tablet screen.
+const maxElementsTablet = 7;
+// Maximum number of logos to be displayed on a desktop screen.
+const maxElementsDesktop = 10;
+// Total number of available payment methods from https://woocommerce.com/document/woopayments/payment-methods.
+const totalPaymentMethods = 20;
+
+export const WooPaymentMethodsLogos: React.VFC< {
+ isWooPayEligible: boolean;
+ maxElements: number;
+} > = ( { isWooPayEligible = false, maxElements = maxElementsDesktop } ) => {
+ const [ maxShownElements, setMaxShownElements ] = useState( maxElements );
+
+ // Determine the maximum number of logos to display, taking into account WooPay’s eligibility.
+ const getMaxShownElements = (
+ maxElementsNumber: number,
+ isWooPayAvailable: boolean
+ ) => {
+ if ( ! isWooPayAvailable ) {
+ return maxElementsNumber + 1;
+ }
+
+ return maxElementsNumber;
+ };
+
+ useEffect( () => {
+ const updateMaxElements = () => {
+ if ( window.innerWidth <= 480 ) {
+ setMaxShownElements( maxElementsMobile );
+ } else if ( window.innerWidth <= 768 ) {
+ setMaxShownElements( maxElementsTablet );
+ } else {
+ setMaxShownElements( maxElements );
+ }
+ };
+
+ updateMaxElements();
+
+ window.addEventListener( 'resize', updateMaxElements );
+
+ return () => {
+ window.removeEventListener( 'resize', updateMaxElements );
+ };
+ }, [ maxElements ] );
+
+ return (
+ <>
+
+ { PaymentMethods.slice(
+ 0,
+ getMaxShownElements( maxShownElements, isWooPayEligible )
+ ).map( ( pm ) => {
+ if ( ! isWooPayEligible && pm.name === 'woopay' ) {
+ return null;
+ }
+
+ return pm.component;
+ } ) }
+ { maxShownElements < totalPaymentMethods && (
+
+ + { totalPaymentMethods - maxShownElements }
+
+ ) }
+
+ >
+ );
+};
diff --git a/packages/js/onboarding/src/components/WCPayBanner/Icons/woopay.js b/packages/js/onboarding/src/images/payment-methods/woopay.js
similarity index 99%
rename from packages/js/onboarding/src/components/WCPayBanner/Icons/woopay.js
rename to packages/js/onboarding/src/images/payment-methods/woopay.js
index 665e104c93e..5ce6e3df931 100644
--- a/packages/js/onboarding/src/components/WCPayBanner/Icons/woopay.js
+++ b/packages/js/onboarding/src/images/payment-methods/woopay.js
@@ -3,7 +3,7 @@
*/
import { createElement } from '@wordpress/element';
-export const WooPay = () => (
+export default () => (
/* eslint-disable */
{
expect( findCountryOption( countryStateOptions, location ) ).toBeNull();
} );
- it( 'should ignore accents for comparsion', () => {
+ it( 'should ignore accents for comparison', () => {
const location = {
city: 'Malaga',
region: 'Andalucia',
diff --git a/packages/js/product-editor/changelog/add-49721_add_readme_for_validators b/packages/js/product-editor/changelog/add-49721_add_readme_for_validators
new file mode 100644
index 00000000000..a954197ddcb
--- /dev/null
+++ b/packages/js/product-editor/changelog/add-49721_add_readme_for_validators
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add readme for validations #49723
diff --git a/packages/js/product-editor/changelog/add-core-combobox b/packages/js/product-editor/changelog/add-core-combobox
new file mode 100644
index 00000000000..5b40961683c
--- /dev/null
+++ b/packages/js/product-editor/changelog/add-core-combobox
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add a ComboboxControl component
diff --git a/packages/js/experimental/changelog/dev-upgrade-ts-5 b/packages/js/product-editor/changelog/dev-46768_add_context_to_errors
similarity index 51%
rename from packages/js/experimental/changelog/dev-upgrade-ts-5
rename to packages/js/product-editor/changelog/dev-46768_add_context_to_errors
index 0785aabfbbf..5b4f15eb7da 100644
--- a/packages/js/experimental/changelog/dev-upgrade-ts-5
+++ b/packages/js/product-editor/changelog/dev-46768_add_context_to_errors
@@ -1,4 +1,4 @@
Significance: minor
Type: dev
-Upgrade TypeScript to 5.1.6
+Add context to errors #49242
diff --git a/packages/js/product-editor/changelog/dev-46768_add_link b/packages/js/product-editor/changelog/dev-46768_add_link
new file mode 100644
index 00000000000..a5ca166965a
--- /dev/null
+++ b/packages/js/product-editor/changelog/dev-46768_add_link
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Add link into the error snackbar #49246
diff --git a/packages/js/product-editor/changelog/dev-46768_add_ref_to_number_control b/packages/js/product-editor/changelog/dev-46768_add_ref_to_number_control
new file mode 100644
index 00000000000..4f9650d9d7d
--- /dev/null
+++ b/packages/js/product-editor/changelog/dev-46768_add_ref_to_number_control
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Add reference to number control #49357
diff --git a/packages/js/product-editor/changelog/dev-46768_add_tests_for_hooks b/packages/js/product-editor/changelog/dev-46768_add_tests_for_hooks
new file mode 100644
index 00000000000..ffca7f43a20
--- /dev/null
+++ b/packages/js/product-editor/changelog/dev-46768_add_tests_for_hooks
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Add tests for snackbar error handling hooks #49498
diff --git a/packages/js/product-editor/changelog/dev-49582_use_custom_link_for_sku_error b/packages/js/product-editor/changelog/dev-49582_use_custom_link_for_sku_error
new file mode 100644
index 00000000000..23df034c730
--- /dev/null
+++ b/packages/js/product-editor/changelog/dev-49582_use_custom_link_for_sku_error
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+[Enhancement]: Change link label for duplicated SKU #49729
diff --git a/packages/js/product-editor/changelog/fix-46638 b/packages/js/product-editor/changelog/fix-46638
new file mode 100644
index 00000000000..1ac315d1bd8
--- /dev/null
+++ b/packages/js/product-editor/changelog/fix-46638
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix clicking outside of the attribute modal triggers an error
diff --git a/packages/js/product-editor/changelog/fix-49345_save_product_check_for_variations b/packages/js/product-editor/changelog/fix-49345_save_product_check_for_variations
new file mode 100644
index 00000000000..f4c9f8db914
--- /dev/null
+++ b/packages/js/product-editor/changelog/fix-49345_save_product_check_for_variations
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Always count variations without price #50129
diff --git a/packages/js/product-editor/changelog/fix-49402 b/packages/js/product-editor/changelog/fix-49402
new file mode 100644
index 00000000000..a5275de5a45
--- /dev/null
+++ b/packages/js/product-editor/changelog/fix-49402
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix unhandled promise rejection when creating an attribute term and the create request fails
diff --git a/packages/js/product-editor/changelog/fix-hide-variation-notice b/packages/js/product-editor/changelog/fix-hide-variation-notice
new file mode 100644
index 00000000000..b317b318d5b
--- /dev/null
+++ b/packages/js/product-editor/changelog/fix-hide-variation-notice
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Hide variation related notices when product type is not variable
diff --git a/packages/js/product-editor/changelog/fix-high-number-currency-round b/packages/js/product-editor/changelog/fix-high-number-currency-round
new file mode 100644
index 00000000000..710258afc09
--- /dev/null
+++ b/packages/js/product-editor/changelog/fix-high-number-currency-round
@@ -0,0 +1,5 @@
+Significance: patch
+Type: fix
+Comment: Prevent high numbers from breaking currency inputs
+
+
diff --git a/packages/js/product-editor/changelog/tweak-variations-notice-styling b/packages/js/product-editor/changelog/tweak-variations-notice-styling
new file mode 100644
index 00000000000..60cb1c5becc
--- /dev/null
+++ b/packages/js/product-editor/changelog/tweak-variations-notice-styling
@@ -0,0 +1,5 @@
+Significance: patch
+Type: tweak
+Comment: Adjust spacings for variations table notice
+
+
diff --git a/packages/js/product-editor/changelog/update-abrev-global-unique-id b/packages/js/product-editor/changelog/update-abrev-global-unique-id
new file mode 100644
index 00000000000..f79328e4f97
--- /dev/null
+++ b/packages/js/product-editor/changelog/update-abrev-global-unique-id
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Allow abbr tags and title attributes in sanitized HTML
diff --git a/packages/js/product-editor/changelog/update-navigation-while-on-input b/packages/js/product-editor/changelog/update-navigation-while-on-input
new file mode 100644
index 00000000000..f1e08a7c3ae
--- /dev/null
+++ b/packages/js/product-editor/changelog/update-navigation-while-on-input
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Allow popovers to exceed modal's limits in taxonomy
diff --git a/packages/js/product-editor/changelog/update-product-description-mobile b/packages/js/product-editor/changelog/update-product-description-mobile
new file mode 100644
index 00000000000..41242c52c19
--- /dev/null
+++ b/packages/js/product-editor/changelog/update-product-description-mobile
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Update description editing for mobile
diff --git a/packages/js/product-editor/src/blocks/generic/number/edit.tsx b/packages/js/product-editor/src/blocks/generic/number/edit.tsx
index 7063a2e4378..2e17702b5ba 100644
--- a/packages/js/product-editor/src/blocks/generic/number/edit.tsx
+++ b/packages/js/product-editor/src/blocks/generic/number/edit.tsx
@@ -17,6 +17,7 @@ import { useProductEdits } from '../../../hooks/use-product-edits';
export function Edit( {
attributes,
+ clientId,
context: { postType },
}: ProductEditorBlockEditProps< NumberBlockAttributes > ) {
const blockProps = useWooBlockProps( attributes );
@@ -47,31 +48,40 @@ export function Edit( {
value &&
parseFloat( value ) < min
) {
- return sprintf(
- // translators: %d is the minimum value of the number input.
- __(
- 'Value must be greater than or equal to %d',
- 'woocommerce'
+ return {
+ message: sprintf(
+ // translators: %d is the minimum value of the number input.
+ __(
+ 'Value must be greater than or equal to %d',
+ 'woocommerce'
+ ),
+ min
),
- min
- );
+ context: clientId,
+ };
}
if (
typeof max === 'number' &&
value &&
parseFloat( value ) > max
) {
- return sprintf(
- // translators: %d is the maximum value of the number input.
- __(
- 'Value must be less than or equal to %d',
- 'woocommerce'
+ return {
+ message: sprintf(
+ // translators: %d is the minimum value of the number input.
+ __(
+ 'Value must be less than or equal to %d',
+ 'woocommerce'
+ ),
+ min
),
- max
- );
+ context: clientId,
+ };
}
if ( required && ! value ) {
- return __( 'This field is required.', 'woocommerce' );
+ return {
+ message: __( 'This field is required.', 'woocommerce' ),
+ context: clientId,
+ };
}
},
[ value ]
diff --git a/packages/js/product-editor/src/blocks/generic/taxonomy/editor.scss b/packages/js/product-editor/src/blocks/generic/taxonomy/editor.scss
index 12272d96c27..cc6dea2051c 100644
--- a/packages/js/product-editor/src/blocks/generic/taxonomy/editor.scss
+++ b/packages/js/product-editor/src/blocks/generic/taxonomy/editor.scss
@@ -14,7 +14,10 @@
&__optional {
color: $gray-700;
}
- }
+ .components-modal__content { // make sure that inline popovers can exceed the dialog's limits
+ overflow: visible;
+ }
+ }
.components-base-control {
margin-bottom: $gap;
diff --git a/packages/js/product-editor/src/blocks/generic/text/edit.tsx b/packages/js/product-editor/src/blocks/generic/text/edit.tsx
index e1f75ce8170..8761ddb851f 100644
--- a/packages/js/product-editor/src/blocks/generic/text/edit.tsx
+++ b/packages/js/product-editor/src/blocks/generic/text/edit.tsx
@@ -2,6 +2,7 @@
* External dependencies
*/
import { useWooBlockProps } from '@woocommerce/block-templates';
+import { useMergeRefs } from '@wordpress/compose';
import { Link } from '@woocommerce/components';
import { Product } from '@woocommerce/data';
import { createElement, useRef } from '@wordpress/element';
@@ -20,6 +21,7 @@ import { TextBlockAttributes } from './types';
export function Edit( {
attributes,
+ clientId,
context: { postType },
}: ProductEditorBlockEditProps< TextBlockAttributes > ) {
const blockProps = useWooBlockProps( attributes );
@@ -50,7 +52,11 @@ export function Edit( {
const inputRef = useRef< HTMLInputElement >( null );
- const { error, validate } = useValidation< Product >(
+ const {
+ error,
+ validate,
+ ref: inputValidatorRef,
+ } = useValidation< Product >(
property,
async function validator() {
if ( ! inputRef.current ) return;
@@ -127,7 +133,10 @@ export function Edit( {
input.setCustomValidity( customErrorMessage );
if ( ! input.validity.valid ) {
- return input.validationMessage;
+ return {
+ message: customErrorMessage,
+ context: clientId,
+ };
}
},
[ type, required, pattern, minLength, maxLength, min, max ]
@@ -160,7 +169,7 @@ export function Edit( {
return (
(
+ 'sku',
+ async function skuValidator() {
+ return undefined;
+ },
+ [ sku ]
+ );
+
return (
120 ) {
- return __(
- 'Please enter a product name shorter than 120 characters.',
- 'woocommerce'
- );
+ return {
+ message: __(
+ 'Please enter a product name shorter than 120 characters.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
},
[ name ]
diff --git a/packages/js/product-editor/src/blocks/product-fields/notice-has-variations/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/notice-has-variations/edit.tsx
index 44fab4ccc71..b9cca64f679 100644
--- a/packages/js/product-editor/src/blocks/product-fields/notice-has-variations/edit.tsx
+++ b/packages/js/product-editor/src/blocks/product-fields/notice-has-variations/edit.tsx
@@ -35,8 +35,11 @@ export function Edit( {
'attributes'
);
+ const [ productType ] = useEntityProp( 'postType', 'product', 'type' );
+
const isOptionsNoticeVisible =
- hasAttributesUsedForVariations( productAttributes );
+ hasAttributesUsedForVariations( productAttributes ) &&
+ productType === 'variable';
return (
diff --git a/packages/js/product-editor/src/blocks/product-fields/product-details-section-description/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/product-details-section-description/edit.tsx
index 3c2aa01acdb..afa73552774 100644
--- a/packages/js/product-editor/src/blocks/product-fields/product-details-section-description/edit.tsx
+++ b/packages/js/product-editor/src/blocks/product-fields/product-details-section-description/edit.tsx
@@ -33,10 +33,7 @@ import { ProductEditorSettings } from '../../../components';
import { BlockFill } from '../../../components/block-slot-fill';
import { useValidations } from '../../../contexts/validation-context';
import { TRACKS_SOURCE } from '../../../constants';
-import {
- WPError,
- getProductErrorMessageAndProps,
-} from '../../../utils/get-product-error-message-and-props';
+import { WPError, useErrorHandler } from '../../../hooks/use-error-handler';
import type {
ProductEditorBlockEditProps,
ProductFormPostProps,
@@ -54,6 +51,8 @@ export function ProductDetailsSectionDescriptionBlockEdit( {
}: ProductEditorBlockEditProps< ProductDetailsSectionDescriptionBlockAttributes > ) {
const blockProps = useWooBlockProps( attributes );
+ const { getProductErrorMessageAndProps } = useErrorHandler();
+
const { productTemplates, productTemplate: selectedProductTemplate } =
useSelect( ( select ) => {
const { getEditorSettings } = select( 'core/editor' );
diff --git a/packages/js/product-editor/src/blocks/product-fields/regular-price/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/regular-price/edit.tsx
index 0089135f397..7743236ae5e 100644
--- a/packages/js/product-editor/src/blocks/product-fields/regular-price/edit.tsx
+++ b/packages/js/product-editor/src/blocks/product-fields/regular-price/edit.tsx
@@ -67,23 +67,35 @@ export function Edit( {
const listPrice = Number.parseFloat( regularPrice );
if ( listPrice ) {
if ( listPrice < 0 ) {
- return __(
- 'Regular price must be greater than or equals to zero.',
- 'woocommerce'
- );
+ return {
+ message: __(
+ 'Regular price must be greater than or equals to zero.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
if (
salePrice &&
listPrice <= Number.parseFloat( salePrice )
) {
- return __(
- 'Regular price must be greater than the sale price.',
- 'woocommerce'
- );
+ return {
+ message: __(
+ 'Regular price must be greater than the sale price.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
} else if ( isRequired ) {
- /* translators: label of required field. */
- return sprintf( __( '%s is required.', 'woocommerce' ), label );
+ return {
+ message: sprintf(
+ /* translators: label of required field. */
+ __( '%s is required.', 'woocommerce' ),
+ label
+ ),
+ context: clientId,
+ };
}
},
[ regularPrice, salePrice ]
diff --git a/packages/js/product-editor/src/blocks/product-fields/sale-price/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/sale-price/edit.tsx
index 0e23cda14bf..305a3e905e7 100644
--- a/packages/js/product-editor/src/blocks/product-fields/sale-price/edit.tsx
+++ b/packages/js/product-editor/src/blocks/product-fields/sale-price/edit.tsx
@@ -59,20 +59,26 @@ export function Edit( {
async function salePriceValidator() {
if ( salePrice ) {
if ( Number.parseFloat( salePrice ) < 0 ) {
- return __(
- 'Sale price must be greater than or equals to zero.',
- 'woocommerce'
- );
+ return {
+ message: __(
+ 'Sale price must be greater than or equals to zero.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
const listPrice = Number.parseFloat( regularPrice );
if (
! listPrice ||
listPrice <= Number.parseFloat( salePrice )
) {
- return __(
- 'Sale price must be lower than the regular price.',
- 'woocommerce'
- );
+ return {
+ message: __(
+ 'Sale price must be lower than the regular price.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
}
},
diff --git a/packages/js/product-editor/src/blocks/product-fields/schedule-sale/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/schedule-sale/edit.tsx
index 92880696252..0c13e1adca2 100644
--- a/packages/js/product-editor/src/blocks/product-fields/schedule-sale/edit.tsx
+++ b/packages/js/product-editor/src/blocks/product-fields/schedule-sale/edit.tsx
@@ -100,14 +100,23 @@ export function Edit( {
async function dateOnSaleFromValidator() {
if ( showScheduleSale && dateOnSaleFromGmt ) {
if ( ! _dateOnSaleFrom.isValid() ) {
- return __( 'Please enter a valid date.', 'woocommerce' );
+ return {
+ message: __(
+ 'Please enter a valid date.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
if ( _dateOnSaleFrom.isAfter( _dateOnSaleTo ) ) {
- return __(
- 'The start date of the sale must be before the end date.',
- 'woocommerce'
- );
+ return {
+ message: __(
+ 'The start date of the sale must be before the end date.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
}
},
@@ -123,14 +132,23 @@ export function Edit( {
async function dateOnSaleToValidator() {
if ( showScheduleSale && dateOnSaleToGmt ) {
if ( ! _dateOnSaleTo.isValid() ) {
- return __( 'Please enter a valid date.', 'woocommerce' );
+ return {
+ message: __(
+ 'Please enter a valid date.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
if ( _dateOnSaleTo.isBefore( _dateOnSaleFrom ) ) {
- return __(
- 'The end date of the sale must be after the start date.',
- 'woocommerce'
- );
+ return {
+ message: __(
+ 'The end date of the sale must be after the start date.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
}
},
diff --git a/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/edit.tsx
index 041f40ba721..7e67365b0ee 100644
--- a/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/edit.tsx
+++ b/packages/js/product-editor/src/blocks/product-fields/shipping-dimensions/edit.tsx
@@ -88,57 +88,89 @@ export function Edit( {
};
}
+ const widthFieldId = `dimensions_width-${ clientId }`;
+
const {
ref: dimensionsWidthRef,
error: dimensionsWidthValidationError,
validate: validateDimensionsWidth,
} = useValidation< Product >(
- `dimensions_width-${ clientId }`,
+ widthFieldId,
async function dimensionsWidthValidator() {
if ( dimensions?.width && +dimensions.width <= 0 ) {
- return __( 'Width must be greater than zero.', 'woocommerce' );
+ return {
+ message: __(
+ 'Width must be greater than zero.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
},
[ dimensions?.width ]
);
+ const lengthFieldId = `dimensions_length-${ clientId }`;
+
const {
ref: dimensionsLengthRef,
error: dimensionsLengthValidationError,
validate: validateDimensionsLength,
} = useValidation< Product >(
- `dimensions_length-${ clientId }`,
+ lengthFieldId,
async function dimensionsLengthValidator() {
if ( dimensions?.length && +dimensions.length <= 0 ) {
- return __( 'Length must be greater than zero.', 'woocommerce' );
+ return {
+ message: __(
+ 'Length must be greater than zero.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
},
[ dimensions?.length ]
);
+ const heightFieldId = `dimensions_height-${ clientId }`;
+
const {
ref: dimensionsHeightRef,
error: dimensionsHeightValidationError,
validate: validateDimensionsHeight,
} = useValidation< Product >(
- `dimensions_height-${ clientId }`,
+ heightFieldId,
async function dimensionsHeightValidator() {
if ( dimensions?.height && +dimensions.height <= 0 ) {
- return __( 'Height must be greater than zero.', 'woocommerce' );
+ return {
+ message: __(
+ 'Height must be greater than zero.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
},
[ dimensions?.height ]
);
+ const weightFieldId = `weight-${ clientId }`;
+
const {
ref: weightRef,
error: weightValidationError,
validate: validateWeight,
} = useValidation< Product >(
- `weight-${ clientId }`,
+ weightFieldId,
async function weightValidator() {
if ( weight && +weight <= 0 ) {
- return __( 'Weight must be greater than zero.', 'woocommerce' );
+ return {
+ message: __(
+ 'Weight must be greater than zero.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
},
[ weight ]
@@ -148,18 +180,22 @@ export function Edit( {
...getDimensionsControlProps( 'width', 'A' ),
ref: dimensionsWidthRef,
onBlur: validateDimensionsWidth,
+ id: widthFieldId,
};
const dimensionsLengthProps = {
...getDimensionsControlProps( 'length', 'B' ),
ref: dimensionsLengthRef,
onBlur: validateDimensionsLength,
+ id: lengthFieldId,
};
const dimensionsHeightProps = {
...getDimensionsControlProps( 'height', 'C' ),
ref: dimensionsHeightRef,
onBlur: validateDimensionsHeight,
+ id: heightFieldId,
};
const weightProps = {
+ id: weightFieldId,
name: 'weight',
value: weight ?? '',
onChange: setWeight,
diff --git a/packages/js/product-editor/src/blocks/product-fields/variation-items/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/variation-items/edit.tsx
index a6f77557cf4..86bda959469 100644
--- a/packages/js/product-editor/src/blocks/product-fields/variation-items/edit.tsx
+++ b/packages/js/product-editor/src/blocks/product-fields/variation-items/edit.tsx
@@ -32,6 +32,7 @@ import { EmptyState } from '../../../components/empty-state';
export function Edit( {
attributes,
+ clientId,
context: { isInSelectedTab },
}: ProductEditorBlockEditProps< VariationOptionsBlockAttributes > ) {
const noticeDimissed = useRef( false );
@@ -79,19 +80,14 @@ export function Edit( {
);
return {
- totalCountWithoutPrice:
- isInSelectedTab && productHasOptions
- ? getProductVariationsTotalCount< number >(
- totalCountWithoutPriceRequestParams
- )
- : 0,
+ totalCountWithoutPrice: productHasOptions
+ ? getProductVariationsTotalCount< number >(
+ totalCountWithoutPriceRequestParams
+ )
+ : 0,
};
},
- [
- isInSelectedTab,
- productHasOptions,
- totalCountWithoutPriceRequestParams,
- ]
+ [ productHasOptions, totalCountWithoutPriceRequestParams ]
);
const {
@@ -125,10 +121,13 @@ export function Edit( {
},
} );
}
- return __(
- 'Set variation prices before adding this product.',
- 'woocommerce'
- );
+ return {
+ message: __(
+ 'Set variation prices before adding this product.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
}
},
[ totalCountWithoutPrice ]
diff --git a/packages/js/product-editor/src/components/attribute-control/attribute-table-row.tsx b/packages/js/product-editor/src/components/attribute-control/attribute-table-row.tsx
index 4796fa06e64..21edc184ec8 100644
--- a/packages/js/product-editor/src/components/attribute-control/attribute-table-row.tsx
+++ b/packages/js/product-editor/src/components/attribute-control/attribute-table-row.tsx
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+import { __, sprintf } from '@wordpress/i18n';
import {
createElement,
useEffect,
@@ -12,7 +13,12 @@ import {
Button,
FormTokenField as CoreFormTokenField,
} from '@wordpress/components';
-import { useSelect, useDispatch, select as sel } from '@wordpress/data';
+import {
+ useSelect,
+ useDispatch,
+ select as sel,
+ dispatch,
+} from '@wordpress/data';
import { cleanForSlug } from '@wordpress/url';
import {
EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME,
@@ -281,22 +287,39 @@ export const AttributeTableRow: React.FC< AttributeTableRowProps > = ( {
// Create the new terms.
const promises = newTokens.map( async ( token ) => {
- const newTerm = ( await createProductAttributeTerm(
- {
- name: token.value,
- slug: token.slug,
- attribute_id: attributeId,
- },
- {
- optimisticQueryUpdate: selectItemsQuery,
- optimisticUrlParameters: [ attributeId ],
- }
- ) ) as ProductAttributeTerm;
+ try {
+ const newTerm = ( await createProductAttributeTerm(
+ {
+ name: token.value,
+ slug: token.slug,
+ attribute_id: attributeId,
+ },
+ {
+ optimisticQueryUpdate: selectItemsQuery,
+ optimisticUrlParameters: [ attributeId ],
+ }
+ ) ) satisfies ProductAttributeTerm;
- return newTerm;
+ return newTerm;
+ } catch ( error ) {
+ dispatch( 'core/notices' ).createErrorNotice(
+ sprintf(
+ /* translators: %s: the attribute term */
+ __(
+ 'There was an error trying to create the attribute term "%s".',
+ 'woocommerce'
+ ),
+ token.value
+ )
+ );
+ return undefined;
+ }
} );
const newTerms = await Promise.all( promises );
+ const storedTerms = newTerms.filter(
+ ( term ) => term !== undefined
+ ) as ProductAttributeTerm[];
// Remove the recently created terms from the temporary state,
setTemporaryTerms( ( prevTerms ) =>
@@ -324,7 +347,11 @@ export const AttributeTableRow: React.FC< AttributeTableRowProps > = ( {
);
// Call the callback to update the Form terms.
- onTermsSelect( [ ...newSelectedTerms, ...newTerms ], index, attribute );
+ onTermsSelect(
+ [ ...newSelectedTerms, ...storedTerms ],
+ index,
+ attribute
+ );
}
/*
diff --git a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx
index f5302d8e4ce..9916f0c8141 100644
--- a/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx
+++ b/packages/js/product-editor/src/components/attribute-control/new-attribute-modal.tsx
@@ -363,7 +363,7 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( {
| React.MouseEvent< Element >
| React.FocusEvent< Element >
) => {
- if ( ! event.isPropagationStopped() ) {
+ if ( ! event?.isPropagationStopped() ) {
onCancel();
}
} }
diff --git a/packages/js/product-editor/src/components/block-editor/block-editor.tsx b/packages/js/product-editor/src/components/block-editor/block-editor.tsx
index 51c917b3baa..be1872baeb2 100644
--- a/packages/js/product-editor/src/components/block-editor/block-editor.tsx
+++ b/packages/js/product-editor/src/components/block-editor/block-editor.tsx
@@ -355,6 +355,11 @@ export function BlockEditor( {
dispatch( productEditorUiStore ).closeModalEditor
}
title={ __( 'Edit description', 'woocommerce' ) }
+ name={
+ product.name === 'AUTO-DRAFT'
+ ? __( '(no product name)', 'woocommerce' )
+ : product.name
+ }
/>
);
diff --git a/packages/js/product-editor/src/components/combobox-control/combobox-control.tsx b/packages/js/product-editor/src/components/combobox-control/combobox-control.tsx
new file mode 100644
index 00000000000..51c657f885f
--- /dev/null
+++ b/packages/js/product-editor/src/components/combobox-control/combobox-control.tsx
@@ -0,0 +1,127 @@
+/**
+ * External dependencies
+ */
+import type { ForwardedRef } from 'react';
+import { ComboboxControl as Combobox } from '@wordpress/components';
+import { useInstanceId } from '@wordpress/compose';
+import {
+ createElement,
+ forwardRef,
+ useEffect,
+ useLayoutEffect,
+ useRef,
+} from '@wordpress/element';
+import classNames from 'classnames';
+
+/**
+ * Internal dependencies
+ */
+import type { ComboboxControlProps } from './types';
+
+/**
+ * This is a wrapper + a work around the Combobox to
+ * expose important properties and events from the
+ * internal input element that are required when
+ * validating the field in the context of a form
+ */
+export const ComboboxControl = forwardRef( function ForwardedComboboxControl(
+ {
+ id,
+ name,
+ allowReset,
+ className,
+ help,
+ hideLabelFromVision,
+ label,
+ messages,
+ value,
+ options,
+ onFilterValueChange,
+ onChange,
+ onBlur,
+ }: ComboboxControlProps,
+ ref: ForwardedRef< HTMLInputElement >
+) {
+ const inputElementRef = useRef< HTMLInputElement >();
+ const generatedId = useInstanceId(
+ ComboboxControl,
+ 'woocommerce-combobox-control'
+ ) as string;
+ const currentId = id ?? generatedId;
+
+ useLayoutEffect(
+ /**
+ * The Combobox component does not expose the ref to the
+ * internal native input element removing the ability to
+ * focus the element when validating it in the context
+ * of a form
+ */
+ function initializeRefs() {
+ inputElementRef.current = document.querySelector(
+ `.${ currentId } [role="combobox"]`
+ ) as HTMLInputElement;
+
+ if ( name ) {
+ inputElementRef.current?.setAttribute( 'name', name );
+ }
+
+ if ( ref ) {
+ if ( typeof ref === 'function' ) {
+ ref( inputElementRef.current );
+ } else {
+ ref.current = inputElementRef.current;
+ }
+ }
+ },
+ [ currentId, name, ref ]
+ );
+
+ useEffect(
+ function overrideBlur() {
+ /**
+ * The Combobox component clear the value of its internal
+ * input control when losing the focus, even when the
+ * selected value is set, affecting the validation behavior
+ * on bluring
+ */
+ function handleBlur( event: FocusEvent ) {
+ onBlur?.( {
+ ...event,
+ target: {
+ ...event.target,
+ value,
+ },
+ } as never );
+ }
+
+ inputElementRef.current?.addEventListener( 'blur', handleBlur );
+
+ return () => {
+ inputElementRef.current?.removeEventListener(
+ 'blur',
+ handleBlur
+ );
+ };
+ },
+ [ value, onBlur ]
+ );
+
+ return (
+
+ );
+} );
diff --git a/packages/js/product-editor/src/components/combobox-control/index.ts b/packages/js/product-editor/src/components/combobox-control/index.ts
new file mode 100644
index 00000000000..0c5d351e22e
--- /dev/null
+++ b/packages/js/product-editor/src/components/combobox-control/index.ts
@@ -0,0 +1,2 @@
+export * from './combobox-control';
+export * from './types';
diff --git a/packages/js/product-editor/src/components/custom-fields/custom-field-name-control/style.scss b/packages/js/product-editor/src/components/combobox-control/style.scss
similarity index 96%
rename from packages/js/product-editor/src/components/custom-fields/custom-field-name-control/style.scss
rename to packages/js/product-editor/src/components/combobox-control/style.scss
index b0263f5d19a..030b9bfda3b 100644
--- a/packages/js/product-editor/src/components/custom-fields/custom-field-name-control/style.scss
+++ b/packages/js/product-editor/src/components/combobox-control/style.scss
@@ -1,4 +1,4 @@
-.woocommerce-custom-field-name-control {
+.woocommerce-combobox-control {
background-color: #fff;
&.has-error {
diff --git a/packages/js/product-editor/src/components/combobox-control/types.ts b/packages/js/product-editor/src/components/combobox-control/types.ts
new file mode 100644
index 00000000000..67e78f9fe79
--- /dev/null
+++ b/packages/js/product-editor/src/components/combobox-control/types.ts
@@ -0,0 +1,13 @@
+/**
+ * External dependencies
+ */
+import { ComboboxControl as Combobox } from '@wordpress/components';
+
+export type ComboboxControlProps = Combobox.Props &
+ Pick<
+ React.DetailedHTMLProps<
+ React.InputHTMLAttributes< HTMLInputElement >,
+ HTMLInputElement
+ >,
+ 'id' | 'name' | 'onBlur'
+ >;
diff --git a/packages/js/product-editor/src/components/custom-fields/custom-field-name-control/custom-field-name-control.tsx b/packages/js/product-editor/src/components/custom-fields/custom-field-name-control/custom-field-name-control.tsx
index 8f17a78672f..44eb7fd03c2 100644
--- a/packages/js/product-editor/src/components/custom-fields/custom-field-name-control/custom-field-name-control.tsx
+++ b/packages/js/product-editor/src/components/custom-fields/custom-field-name-control/custom-field-name-control.tsx
@@ -3,24 +3,20 @@
*/
import type { ForwardedRef } from 'react';
import apiFetch from '@wordpress/api-fetch';
-import { ComboboxControl } from '@wordpress/components';
-import { useDebounce, useInstanceId } from '@wordpress/compose';
+import { useDebounce } from '@wordpress/compose';
import {
createElement,
forwardRef,
useCallback,
- useEffect,
- useLayoutEffect,
useMemo,
- useRef,
useState,
} from '@wordpress/element';
import { addQueryArgs } from '@wordpress/url';
-import classNames from 'classnames';
/**
* Internal dependencies
*/
+import { ComboboxControl, ComboboxControlProps } from '../../combobox-control';
import type { ComboboxControlOption } from '../../attribute-combobox-field/types';
import type { CustomFieldNameControlProps } from './types';
@@ -56,35 +52,13 @@ async function searchCustomFieldNames( search?: string ) {
} );
}
-/**
- * This is a wrapper + a work around the Combobox to
- * expose important properties and events from the
- * internal input element that are required when
- * validating the field in the context of a form
- */
export const CustomFieldNameControl = forwardRef(
function ForwardedCustomFieldNameControl(
- {
- allowReset,
- className,
- help,
- hideLabelFromVision,
- label,
- messages,
- value,
- onChange,
- onBlur,
- }: CustomFieldNameControlProps,
+ { value, onBlur, ...props }: CustomFieldNameControlProps,
ref: ForwardedRef< HTMLInputElement >
) {
- const inputElementRef = useRef< HTMLInputElement >();
- const id = useInstanceId(
- CustomFieldNameControl,
- 'woocommerce-custom-field-name'
- );
-
const [ customFieldNames, setCustomFieldNames ] = useState<
- ComboboxControl.Props[ 'options' ]
+ ComboboxControlProps[ 'options' ]
>( [] );
const options = useMemo(
@@ -109,29 +83,6 @@ export const CustomFieldNameControl = forwardRef(
[ customFieldNames, value ]
);
- useLayoutEffect(
- /**
- * The Combobox component does not expose the ref to the
- * internal native input element removing the ability to
- * focus the element when validating it in the context
- * of a form
- */
- function initializeRefs() {
- inputElementRef.current = document.querySelector(
- `.${ id } [role="combobox"]`
- ) as HTMLInputElement;
-
- if ( ref ) {
- if ( typeof ref === 'function' ) {
- ref( inputElementRef.current );
- } else {
- ref.current = inputElementRef.current;
- }
- }
- },
- [ id, ref ]
- );
-
const handleFilterValueChange = useDebounce(
useCallback(
function onFilterValueChange( search: string ) {
@@ -144,50 +95,19 @@ export const CustomFieldNameControl = forwardRef(
250
);
- useEffect(
- function overrideBlur() {
- /**
- * The Combobox component clear the value of its internal
- * input control when losing the focus, even when the
- * selected value is set, afecting the validation behavior
- * on bluring
- */
- function handleBlur( event: FocusEvent ) {
- setCustomFieldNames( [] );
- if ( inputElementRef.current ) {
- inputElementRef.current.value = value;
- }
- onBlur?.( event as never );
- }
-
- inputElementRef.current?.addEventListener( 'blur', handleBlur );
-
- return () => {
- inputElementRef.current?.removeEventListener(
- 'blur',
- handleBlur
- );
- };
- },
- [ value, onBlur ]
- );
+ function handleBlur( event: React.FocusEvent< HTMLInputElement > ) {
+ setCustomFieldNames( [] );
+ onBlur?.( event );
+ }
return (
);
}
diff --git a/packages/js/product-editor/src/components/custom-fields/style.scss b/packages/js/product-editor/src/components/custom-fields/style.scss
index 8560c2aadeb..6a4137cc9c8 100644
--- a/packages/js/product-editor/src/components/custom-fields/style.scss
+++ b/packages/js/product-editor/src/components/custom-fields/style.scss
@@ -1,6 +1,5 @@
@import "./create-modal/style.scss";
@import "./edit-modal/style.scss";
-@import "./custom-field-name-control/style.scss";
.woocommerce-product-custom-fields {
&__table {
diff --git a/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx b/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx
index 2d09b1b9e27..e3302b6be98 100644
--- a/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx
+++ b/packages/js/product-editor/src/components/header/hooks/use-preview/use-preview.tsx
@@ -13,7 +13,7 @@ import { MouseEvent } from 'react';
* Internal dependencies
*/
import { useValidations } from '../../../../contexts/validation-context';
-import { WPError } from '../../../../utils/get-product-error-message-and-props';
+import { WPError } from '../../../../hooks/use-error-handler';
import { useProductURL } from '../../../../hooks/use-product-url';
import { PreviewButtonProps } from '../../preview-button';
import { errorHandler } from '../../../../hooks/use-product-manager';
diff --git a/packages/js/product-editor/src/components/header/hooks/use-publish/use-publish.tsx b/packages/js/product-editor/src/components/header/hooks/use-publish/use-publish.tsx
index a8a0e1fd795..53199a11455 100644
--- a/packages/js/product-editor/src/components/header/hooks/use-publish/use-publish.tsx
+++ b/packages/js/product-editor/src/components/header/hooks/use-publish/use-publish.tsx
@@ -13,7 +13,7 @@ import { useShortcut } from '@wordpress/keyboard-shortcuts';
*/
import { useProductManager } from '../../../../hooks/use-product-manager';
import { useProductScheduled } from '../../../../hooks/use-product-scheduled';
-import type { WPError } from '../../../../utils/get-product-error-message-and-props';
+import type { WPError } from '../../../../hooks/use-error-handler';
import type { PublishButtonProps } from '../../publish-button';
export function usePublish< T = Product >( {
diff --git a/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx b/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx
index ed37a30a339..d1e2a64bc0b 100644
--- a/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx
+++ b/packages/js/product-editor/src/components/header/hooks/use-save-draft/use-save-draft.tsx
@@ -15,7 +15,7 @@ import { useShortcut } from '@wordpress/keyboard-shortcuts';
* Internal dependencies
*/
import { useValidations } from '../../../../contexts/validation-context';
-import { WPError } from '../../../../utils/get-product-error-message-and-props';
+import { WPError } from '../../../../hooks/use-error-handler';
import { SaveDraftButtonProps } from '../../save-draft-button';
import { recordProductEvent } from '../../../../utils/record-product-event';
import { errorHandler } from '../../../../hooks/use-product-manager';
diff --git a/packages/js/product-editor/src/components/header/preview-button/preview-button.tsx b/packages/js/product-editor/src/components/header/preview-button/preview-button.tsx
index 3eb10c03011..2a1872a1ef8 100644
--- a/packages/js/product-editor/src/components/header/preview-button/preview-button.tsx
+++ b/packages/js/product-editor/src/components/header/preview-button/preview-button.tsx
@@ -11,7 +11,7 @@ import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
-import { getProductErrorMessageAndProps } from '../../../utils/get-product-error-message-and-props';
+import { useErrorHandler } from '../../../hooks/use-error-handler';
import { usePreview } from '../hooks/use-preview';
import { PreviewButtonProps } from './types';
import { TRACKS_SOURCE } from '../../../constants';
@@ -22,6 +22,7 @@ export function PreviewButton( {
...props
}: PreviewButtonProps ) {
const { createErrorNotice } = useDispatch( 'core/notices' );
+ const { getProductErrorMessageAndProps } = useErrorHandler();
const previewButtonProps = usePreview( {
productStatus,
diff --git a/packages/js/product-editor/src/components/header/publish-button/publish-button-menu/publish-button-menu.tsx b/packages/js/product-editor/src/components/header/publish-button/publish-button-menu/publish-button-menu.tsx
index e6e4331a706..d74daadd8f5 100644
--- a/packages/js/product-editor/src/components/header/publish-button/publish-button-menu/publish-button-menu.tsx
+++ b/packages/js/product-editor/src/components/header/publish-button/publish-button-menu/publish-button-menu.tsx
@@ -17,7 +17,7 @@ import { recordEvent } from '@woocommerce/tracks';
import { useProductManager } from '../../../../hooks/use-product-manager';
import { useProductScheduled } from '../../../../hooks/use-product-scheduled';
import { recordProductEvent } from '../../../../utils/record-product-event';
-import { getProductErrorMessageAndProps } from '../../../../utils/get-product-error-message-and-props';
+import { useErrorHandler } from '../../../../hooks/use-error-handler';
import { ButtonWithDropdownMenu } from '../../../button-with-dropdown-menu';
import { SchedulePublishModal } from '../../../schedule-publish-modal';
import { showSuccessNotice } from '../utils';
@@ -42,6 +42,7 @@ export function PublishButtonMenu( {
postType,
'status'
);
+ const { getProductErrorMessageAndProps } = useErrorHandler();
function scheduleProduct( dateString?: string ) {
schedule( dateString )
diff --git a/packages/js/product-editor/src/components/header/publish-button/publish-button.tsx b/packages/js/product-editor/src/components/header/publish-button/publish-button.tsx
index 63dc1c326cc..2b00a2cf97a 100644
--- a/packages/js/product-editor/src/components/header/publish-button/publish-button.tsx
+++ b/packages/js/product-editor/src/components/header/publish-button/publish-button.tsx
@@ -14,7 +14,7 @@ import { recordEvent } from '@woocommerce/tracks';
* Internal dependencies
*/
import { store as productEditorUiStore } from '../../../store/product-editor-ui';
-import { getProductErrorMessageAndProps } from '../../../utils/get-product-error-message-and-props';
+import { useErrorHandler } from '../../../hooks/use-error-handler';
import { recordProductEvent } from '../../../utils/record-product-event';
import { useFeedbackBar } from '../../../hooks/use-feedback-bar';
import { TRACKS_SOURCE } from '../../../constants';
@@ -33,6 +33,7 @@ export function PublishButton( {
const { createErrorNotice } = useDispatch( 'core/notices' );
const { maybeShowFeedbackBar } = useFeedbackBar();
const { openPrepublishPanel } = useDispatch( productEditorUiStore );
+ const { getProductErrorMessageAndProps } = useErrorHandler();
const [ editedStatus, , prevStatus ] = useEntityProp< Product[ 'status' ] >(
'postType',
diff --git a/packages/js/product-editor/src/components/header/save-draft-button/save-draft-button.tsx b/packages/js/product-editor/src/components/header/save-draft-button/save-draft-button.tsx
index e36bd765602..6a859f3251e 100644
--- a/packages/js/product-editor/src/components/header/save-draft-button/save-draft-button.tsx
+++ b/packages/js/product-editor/src/components/header/save-draft-button/save-draft-button.tsx
@@ -11,7 +11,7 @@ import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
-import { getProductErrorMessageAndProps } from '../../../utils/get-product-error-message-and-props';
+import { useErrorHandler } from '../../../hooks/use-error-handler';
import { recordProductEvent } from '../../../utils/record-product-event';
import { useSaveDraft } from '../hooks/use-save-draft';
import { SaveDraftButtonProps } from './types';
@@ -28,6 +28,8 @@ export function SaveDraftButton( {
const { maybeShowFeedbackBar } = useFeedbackBar();
+ const { getProductErrorMessageAndProps } = useErrorHandler();
+
const saveDraftButtonProps = useSaveDraft( {
productStatus,
productType,
diff --git a/packages/js/product-editor/src/components/iframe-editor/iframe-editor.scss b/packages/js/product-editor/src/components/iframe-editor/iframe-editor.scss
index c9dfdc696fe..2c92e08edb6 100644
--- a/packages/js/product-editor/src/components/iframe-editor/iframe-editor.scss
+++ b/packages/js/product-editor/src/components/iframe-editor/iframe-editor.scss
@@ -49,6 +49,11 @@
flex-shrink: 0;
height: 100%;
overflow: scroll;
+ @media only screen and (max-width: $break-medium) {
+ width: 100%;
+ position: absolute;
+ z-index: 100;
+ }
}
.block-editor-block-contextual-toolbar.is-fixed {
diff --git a/packages/js/product-editor/src/components/iframe-editor/iframe-editor.tsx b/packages/js/product-editor/src/components/iframe-editor/iframe-editor.tsx
index 3d687d20eea..1edeb2cd7c0 100644
--- a/packages/js/product-editor/src/components/iframe-editor/iframe-editor.tsx
+++ b/packages/js/product-editor/src/components/iframe-editor/iframe-editor.tsx
@@ -62,6 +62,7 @@ type IframeEditorProps = {
onInput?: ( blocks: BlockInstance[] ) => void;
settings?: Partial< EditorSettings & EditorBlockListSettings > | undefined;
showBackButton?: boolean;
+ name: string;
};
export function IframeEditor( {
@@ -70,6 +71,7 @@ export function IframeEditor( {
onInput = () => {},
settings: __settings,
showBackButton = false,
+ name,
}: IframeEditorProps ) {
const [ resizeObserver ] = useResizeObserver();
const [ temporalBlocks, setTemporalBlocks ] = useState< BlockInstance[] >(
@@ -270,7 +272,7 @@ export function IframeEditor( {
{ /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
-
+
diff --git a/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/document-overview-sidebar/document-overview-sidebar.tsx b/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/document-overview-sidebar/document-overview-sidebar.tsx
index c79f960a145..3cb00f0c154 100644
--- a/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/document-overview-sidebar/document-overview-sidebar.tsx
+++ b/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/document-overview-sidebar/document-overview-sidebar.tsx
@@ -91,7 +91,7 @@ export function DocumentOverviewSidebar() {
tabs={ [
{
name: 'list-view',
- title: 'List View',
+ title: __( 'List View', 'woocommerce' ),
className:
'woocommerce-iframe-editor__document-overview-sidebar-tab-item',
},
diff --git a/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/document-overview-sidebar/style.scss b/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/document-overview-sidebar/style.scss
index 0bec179b459..890488bc515 100644
--- a/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/document-overview-sidebar/style.scss
+++ b/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/document-overview-sidebar/style.scss
@@ -3,6 +3,12 @@
width: 350px;
height: 100%;
border-right: 1px solid $gray-400;
+ background: $studio-white;
+ @media only screen and (max-width: $break-medium) {
+ width: 100%;
+ position: absolute;
+ z-index: 100;
+ }
&-close-button {
position: absolute;
@@ -22,6 +28,10 @@
display: flex;
flex-direction: column;
height: calc(100% - $grid-unit-60);
+ .block-editor-list-view-tree {
+ margin: 0;
+ width: 100%;
+ }
}
}
@@ -30,10 +40,5 @@
overflow-x: hidden;
overflow-y: auto;
padding: $grid-unit calc($grid-unit - 2px);
-
- .block-editor-list-view-tree {
- margin: 0;
- width: 100%;
- }
}
}
diff --git a/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/inserter-sidebar.scss b/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/inserter-sidebar.scss
index d993e34fa1f..2b967aae191 100644
--- a/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/inserter-sidebar.scss
+++ b/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/inserter-sidebar.scss
@@ -4,6 +4,12 @@
&-content {
height: 100%;
+ @media only screen and (max-width: $break-medium) {
+ width: 100%;
+ position: absolute;
+ z-index: 100;
+ background-color: $studio-white;
+ }
}
/*
diff --git a/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/inserter-sidebar.tsx b/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/inserter-sidebar.tsx
index ccf40544d28..35cc63515de 100644
--- a/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/inserter-sidebar.tsx
+++ b/packages/js/product-editor/src/components/iframe-editor/secondary-sidebar/inserter-sidebar.tsx
@@ -71,6 +71,11 @@ export default function InserterSidebar() {
rootClientId={ rootClientId }
ref={ libraryRef }
onClose={ closeInserter }
+ onSelect={ () => {
+ if ( isMobileViewport ) {
+ closeInserter();
+ }
+ } }
/>
diff --git a/packages/js/product-editor/src/components/iframe-editor/sidebar/plugin-sidebar/plugin-sidebar.tsx b/packages/js/product-editor/src/components/iframe-editor/sidebar/plugin-sidebar/plugin-sidebar.tsx
index ebea4cc0ccc..3174a052312 100644
--- a/packages/js/product-editor/src/components/iframe-editor/sidebar/plugin-sidebar/plugin-sidebar.tsx
+++ b/packages/js/product-editor/src/components/iframe-editor/sidebar/plugin-sidebar/plugin-sidebar.tsx
@@ -22,6 +22,7 @@ type PluginSidebarProps = {
isActiveByDefault?: boolean;
name?: string;
title?: string;
+ smallScreenTitle: string;
};
export function PluginSidebar( { className, ...props }: PluginSidebarProps ) {
diff --git a/packages/js/product-editor/src/components/iframe-editor/sidebar/settings-sidebar/settings-sidebar.tsx b/packages/js/product-editor/src/components/iframe-editor/sidebar/settings-sidebar/settings-sidebar.tsx
index 09c5093a8fd..4a34d854224 100644
--- a/packages/js/product-editor/src/components/iframe-editor/sidebar/settings-sidebar/settings-sidebar.tsx
+++ b/packages/js/product-editor/src/components/iframe-editor/sidebar/settings-sidebar/settings-sidebar.tsx
@@ -17,7 +17,11 @@ const SettingsHeader = () => {
return { __( 'Settings', 'woocommerce' ) } ;
};
-export const SettingsSidebar = () => {
+export const SettingsSidebar = ( {
+ smallScreenTitle,
+}: {
+ smallScreenTitle: string;
+} ) => {
return (
{
// the pin button in the default header from being displayed.
header={ }
closeLabel={ __( 'Close settings', 'woocommerce' ) }
+ smallScreenTitle={ smallScreenTitle }
>
diff --git a/packages/js/product-editor/src/components/index.ts b/packages/js/product-editor/src/components/index.ts
index c58638dcd16..074645ed104 100644
--- a/packages/js/product-editor/src/components/index.ts
+++ b/packages/js/product-editor/src/components/index.ts
@@ -104,3 +104,8 @@ export {
export { PluginSidebar as __experimentalModalBlockEditorPluginSidebar } from './iframe-editor';
export { PluginMoreMenuItem as __experimentalModalBlockEditorPluginMoreMenuItem } from './iframe-editor';
+
+export {
+ ComboboxControl as __experimentalComboboxControl,
+ type ComboboxControlProps,
+} from './combobox-control';
diff --git a/packages/js/product-editor/src/components/label/label.tsx b/packages/js/product-editor/src/components/label/label.tsx
index cb75b68d352..251bca9c592 100644
--- a/packages/js/product-editor/src/components/label/label.tsx
+++ b/packages/js/product-editor/src/components/label/label.tsx
@@ -1,7 +1,11 @@
/**
* External dependencies
*/
-import { createElement, createInterpolateElement } from '@wordpress/element';
+import {
+ createElement,
+ createInterpolateElement,
+ isValidElement,
+} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Icon, help as helpIcon } from '@wordpress/icons';
import { __experimentalTooltip as Tooltip } from '@woocommerce/components';
@@ -35,7 +39,11 @@ export const Label: React.FC< LabelProps > = ( {
labelElement = createInterpolateElement(
__( ' ', 'woocommerce' ),
{
- label: { label } ,
+ label: (
+
+ ),
note: (
{ note }
@@ -83,11 +91,16 @@ export const Label: React.FC< LabelProps > = ( {
);
}
+ const spanAdditionalProps =
+ typeof labelElement === 'string'
+ ? { dangerouslySetInnerHTML: sanitizeHTML( label ) }
+ : {};
+
return (
{ /* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */ }
-
- { labelElement }
+
+ { isValidElement( labelElement ) ? labelElement : null }
{ tooltip && (
diff --git a/packages/js/product-editor/src/components/modal-editor/modal-editor.tsx b/packages/js/product-editor/src/components/modal-editor/modal-editor.tsx
index a6d36c39267..f70145116ed 100644
--- a/packages/js/product-editor/src/components/modal-editor/modal-editor.tsx
+++ b/packages/js/product-editor/src/components/modal-editor/modal-editor.tsx
@@ -23,6 +23,7 @@ type ModalEditorProps = {
onClose?: () => void;
settings?: Partial< EditorSettings & EditorBlockListSettings > | undefined;
title: string;
+ name: string;
};
export function ModalEditor( {
@@ -30,6 +31,7 @@ export function ModalEditor( {
onChange,
onClose,
title,
+ name,
}: ModalEditorProps ) {
const { closeModalEditor } = useDispatch( productEditorUiStore );
@@ -58,6 +60,7 @@ export function ModalEditor( {
onInput={ debouncedOnChange }
onChange={ debouncedOnChange }
onClose={ handleClose }
+ name={ name }
/>
);
diff --git a/packages/js/product-editor/src/components/modal-editor/style.scss b/packages/js/product-editor/src/components/modal-editor/style.scss
index 1bbd9f9564a..74087df3f28 100644
--- a/packages/js/product-editor/src/components/modal-editor/style.scss
+++ b/packages/js/product-editor/src/components/modal-editor/style.scss
@@ -4,7 +4,11 @@ $modal-editor-height: 60px;
width: 100%;
height: 100%;
max-height: calc( 100% - 40px );
- margin: 20px;
+ margin: $gap-large;
+ @media only screen and (max-width: $break-mobile) {
+ margin: unset;
+ max-height: unset;
+ }
border-radius: 8px;
.components-modal__header {
diff --git a/packages/js/product-editor/src/components/number-control/number-control.tsx b/packages/js/product-editor/src/components/number-control/number-control.tsx
index 13160a9ab8f..bb4a6b8daa8 100644
--- a/packages/js/product-editor/src/components/number-control/number-control.tsx
+++ b/packages/js/product-editor/src/components/number-control/number-control.tsx
@@ -3,6 +3,7 @@
*/
import {
createElement,
+ forwardRef,
Fragment,
isValidElement,
useEffect,
@@ -13,6 +14,7 @@ import { useInstanceId } from '@wordpress/compose';
import classNames from 'classnames';
import { plus, reset } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
+import type { ForwardedRef } from 'react';
import {
BaseControl,
Button,
@@ -27,6 +29,7 @@ import { useNumberInputProps } from '../../hooks/use-number-input-props';
import { Label } from '../label/label';
export type NumberProps = {
+ id?: string;
value: string;
onChange: ( selected: string ) => void;
label: string | JSX.Element;
@@ -47,164 +50,180 @@ export type NumberProps = {
const MEDIUM_DELAY = 500;
const SHORT_DELAY = 100;
-export const NumberControl: React.FC< NumberProps > = ( {
- value,
- onChange,
- label,
- suffix,
- help,
- error,
- onBlur,
- onFocus,
- required,
- tooltip,
- placeholder,
- disabled,
- step = 1,
- min = -Infinity,
- max = Infinity,
-}: NumberProps ) => {
- const id = useInstanceId( BaseControl, 'product_number_field' ) as string;
- const [ isFocused, setIsFocused ] = useState( false );
- const unfocusIfOutside = ( event: React.FocusEvent ) => {
- if (
- ! document
- .getElementById( id )
- ?.parentElement?.contains( event.relatedTarget )
- ) {
- setIsFocused( false );
- onBlur?.();
- }
- };
- const inputProps = useNumberInputProps( {
- value: value || '',
- onChange,
- onFocus: () => {
- setIsFocused( true );
- onFocus?.();
- },
- min,
- max,
- } );
-
- const [ increment, setIncrement ] = useState( 0 );
-
- const timeoutRef = useRef< number | null >( null );
-
- const isInitialClick = useRef< boolean >( false );
-
- const incrementValue = () => {
- const newValue = parseFloat( value || '0' ) + increment;
- if ( newValue >= min && newValue <= max )
- onChange( String( newValue ) );
- };
-
- useEffect( () => {
- if ( increment !== 0 ) {
- timeoutRef.current = setTimeout(
- incrementValue,
- isInitialClick.current ? MEDIUM_DELAY : SHORT_DELAY
- );
- isInitialClick.current = false;
- } else if ( timeoutRef.current ) {
- clearTimeout( timeoutRef.current );
- }
- return () => {
- if ( timeoutRef.current ) {
- clearTimeout( timeoutRef.current );
+export const NumberControl: React.FC< NumberProps > = forwardRef(
+ (
+ {
+ id,
+ value,
+ onChange,
+ label,
+ suffix,
+ help,
+ error,
+ onBlur,
+ onFocus,
+ required,
+ tooltip,
+ placeholder,
+ disabled,
+ step = 1,
+ min = -Infinity,
+ max = Infinity,
+ }: NumberProps,
+ ref: ForwardedRef< HTMLInputElement >
+ ) => {
+ const instanceId = useInstanceId(
+ BaseControl,
+ 'product_number_field'
+ ) as string;
+ const identifier = id ?? instanceId;
+ const [ isFocused, setIsFocused ] = useState( false );
+ const unfocusIfOutside = ( event: React.FocusEvent ) => {
+ if (
+ ! document
+ .getElementById( identifier )
+ ?.parentElement?.contains( event.relatedTarget )
+ ) {
+ setIsFocused( false );
+ onBlur?.();
}
};
- }, [ increment, value ] );
- const resetIncrement = () => setIncrement( 0 );
-
- const handleIncrement = ( thisStep: number ) => {
- const newValue = parseFloat( value || '0' ) + thisStep;
- if ( newValue >= min && newValue <= max ) {
- onChange( String( parseFloat( value || '0' ) + thisStep ) );
- setIncrement( thisStep );
- isInitialClick.current = true;
+ function handleOnFocus() {
+ setIsFocused( true );
+ onFocus?.();
}
- };
- return (
-
- )
+ const inputProps = useNumberInputProps( {
+ value: value || '',
+ onChange,
+ onFocus: handleOnFocus,
+ min,
+ max,
+ } );
+
+ const [ increment, setIncrement ] = useState( 0 );
+
+ const timeoutRef = useRef< number | null >( null );
+
+ const isInitialClick = useRef< boolean >( false );
+
+ function incrementValue() {
+ const newValue = parseFloat( value || '0' ) + increment;
+ if ( newValue >= min && newValue <= max )
+ onChange( String( newValue ) );
+ }
+
+ useEffect( () => {
+ if ( increment !== 0 ) {
+ timeoutRef.current = setTimeout(
+ incrementValue,
+ isInitialClick.current ? MEDIUM_DELAY : SHORT_DELAY
+ );
+ isInitialClick.current = false;
+ } else if ( timeoutRef.current ) {
+ clearTimeout( timeoutRef.current );
}
- help={ error || help }
- >
-
- { suffix }
- { isFocused && (
- <>
- = max
- }
- onMouseDown={ () =>
- handleIncrement( step )
- }
- onMouseLeave={ resetIncrement }
- onMouseUp={ resetIncrement }
- onBlur={ unfocusIfOutside }
- isSmall
- aria-hidden="true"
- aria-label={ __(
- 'Increment',
- 'woocommerce'
- ) }
- tabIndex={ -1 }
- />
-
- handleIncrement( -step )
- }
- onMouseLeave={ resetIncrement }
- onMouseUp={ resetIncrement }
- isSmall
- aria-hidden="true"
- aria-label={ __(
- 'Decrement',
- 'woocommerce'
- ) }
- tabIndex={ -1 }
- />
- >
- ) }
- >
+ return () => {
+ if ( timeoutRef.current ) {
+ clearTimeout( timeoutRef.current );
}
- placeholder={ placeholder }
- onBlur={ unfocusIfOutside }
- />
-
- );
-};
+ };
+ }, [ increment, value ] );
+
+ function resetIncrement() {
+ setIncrement( 0 );
+ }
+
+ function handleIncrement( thisStep: number ) {
+ const newValue = parseFloat( value || '0' ) + thisStep;
+ if ( newValue >= min && newValue <= max ) {
+ onChange( String( parseFloat( value || '0' ) + thisStep ) );
+ setIncrement( thisStep );
+ isInitialClick.current = true;
+ }
+ }
+
+ return (
+
+ )
+ }
+ help={ error || help }
+ >
+
+ { suffix }
+ { isFocused && (
+ <>
+ = max
+ }
+ onMouseDown={ () =>
+ handleIncrement( step )
+ }
+ onMouseLeave={ resetIncrement }
+ onMouseUp={ resetIncrement }
+ onBlur={ unfocusIfOutside }
+ isSmall
+ aria-hidden="true"
+ aria-label={ __(
+ 'Increment',
+ 'woocommerce'
+ ) }
+ tabIndex={ -1 }
+ />
+
+ handleIncrement( -step )
+ }
+ onMouseLeave={ resetIncrement }
+ onMouseUp={ resetIncrement }
+ isSmall
+ aria-hidden="true"
+ aria-label={ __(
+ 'Decrement',
+ 'woocommerce'
+ ) }
+ tabIndex={ -1 }
+ />
+ >
+ ) }
+ >
+ }
+ placeholder={ placeholder }
+ onBlur={ unfocusIfOutside }
+ />
+
+ );
+ }
+);
diff --git a/packages/js/product-editor/src/components/variations-table/styles.scss b/packages/js/product-editor/src/components/variations-table/styles.scss
index 9f31a7fe940..9e78670f16a 100644
--- a/packages/js/product-editor/src/components/variations-table/styles.scss
+++ b/packages/js/product-editor/src/components/variations-table/styles.scss
@@ -68,7 +68,8 @@
&__notice {
border-left: 0px;
- margin: 0 0 $gap-large 0;
+ margin: 0 0 $gap-small 0;
+ padding: $gap-small $gap;
&.is-error {
background-color: #fcf0f1;
}
diff --git a/packages/js/product-editor/src/contexts/validation-context/README.md b/packages/js/product-editor/src/contexts/validation-context/README.md
new file mode 100644
index 00000000000..df3f2c01f03
--- /dev/null
+++ b/packages/js/product-editor/src/contexts/validation-context/README.md
@@ -0,0 +1,75 @@
+# Validations and error handling
+
+This directory contains some components and hooks used for validations in the product editor.
+
+## What happens when there is an error in the form?
+
+1. Fields registered in the validator context [will get validated](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx#L87-L110). A field can be registered by making use of the useValidation hook.
+ - For instance:
+
+ ```javascript
+ const {
+ ref: myRef,
+ error: myValidationError,
+ validate: validateMyField,
+ } = useValidation <
+ Product >
+ ( 'myfield',
+ async function myFieldValidator() {
+ if ( ! myField ) {
+ return {
+ message: 'My error message',
+ context: clientId,
+ };
+ }
+ },
+ [ myField ] );
+ ```
+
+2. If a field has an error, it returns an object consisting of the error/validation message, the context, and the validatorId ([link](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx#L74) ).
+ - The `context` contains the block Id, and the `validatorId` a unique ID for the validator specifically ( generally a prefix with the block id ).
+ - If, for instance, the name field is empty, the validation will fail and will throw an object like this:
+
+ ```javascript
+ { message: 'Product name is required.'; context: [block id]; validatorId: [prefix + block id] }
+ ```
+
+ - This is the result of the name validator and the [validatorId addition](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx#L69).
+
+ ```javascript
+ async function nameValidator() {
+ if ( ! name || name === AUTO_DRAFT_NAME ) {
+ return {
+ message: __( 'Product name is required.', 'woocommerce' ),
+ context: clientId,
+ };
+ }
+
+ if ( name.length > 120 ) {
+ return {
+ message: __(
+ 'Please enter a product name shorter than 120 characters.',
+ 'woocommerce'
+ ),
+ context: clientId,
+ };
+ }
+ },
+ ```
+
+3. If an error is present on the form we will show an error snackbar with the error message. We will actually include a **View error** link if the field with the relevant error is not visible ( like on another tab ). Clicking the **View error** link will direct users to the relevant field.
+ - We create this link by making use of the `context` property (the block id), this makes use of the `useBlocksHelper()` hook to get the parent tab id. We can do this by making use of the `core/block-editor` store and using `getBlockParentsByBlockName` (link to relevant code).
+ - When the field with [the error is not visible](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/hooks/use-error-handler.ts#L105), a link pointing to it will be added to the snackbar.
+ - Otherwise, the error will be dismissed automatically.
+ - The hook `useErrorHandler` is used to get the [error props](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/hooks/use-error-handler.ts#L79).
+ - The error shown will depend [on the error code](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/hooks/use-error-handler.ts#L92).
+ - [As you can see here](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/hooks/use-error-handler.ts#L157-L162), if the error doesn't have a code, the default message will be `Failed to save product.`
+ - The context is used to [get the parent tab](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/hooks/use-blocks-helper/use-blocks-helper.ts#L7) id and the validatorId to [focus on the field](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/hooks/use-error-handler.ts#L68).
+
+Finally, [the snackbar with the error](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/product-editor/src/components/header/publish-button/publish-button.tsx#L70) message and props will be displayed.
+
+![Product editor error snackbar](https://developer.woocommerce.com/wp-content/uploads/sites/2/2024/07/product-editor-error-snack-bar-e1721670028482.png)
+
+## Limitations
+
+The server errors, such as `duplicated SKU`, are not being mapped yet.
diff --git a/packages/js/product-editor/src/contexts/validation-context/types.ts b/packages/js/product-editor/src/contexts/validation-context/types.ts
index 6bfce6e40a2..ba65be0b767 100644
--- a/packages/js/product-editor/src/contexts/validation-context/types.ts
+++ b/packages/js/product-editor/src/contexts/validation-context/types.ts
@@ -12,6 +12,7 @@ export type ValidationContextProps< T > = {
validator: Validator< T >
): React.Ref< HTMLElement >;
unRegisterValidator( validatorId: string ): void;
+ getFieldByValidatorId: ( validatorId: string ) => Promise< HTMLElement >;
validateField(
name: string,
newData?: Record< string, unknown >
@@ -24,7 +25,9 @@ export type ValidationProviderProps = {
productId: number;
};
-export type ValidationError = string | undefined;
+export type ValidationError =
+ | { message?: string; context?: string; validatorId?: string }
+ | undefined;
export type ValidationErrors = Record< string, ValidationError >;
export type ValidatorRegistration< T > = {
diff --git a/packages/js/product-editor/src/contexts/validation-context/use-validation.ts b/packages/js/product-editor/src/contexts/validation-context/use-validation.ts
index 6621d9e8672..c60db5c46dd 100644
--- a/packages/js/product-editor/src/contexts/validation-context/use-validation.ts
+++ b/packages/js/product-editor/src/contexts/validation-context/use-validation.ts
@@ -31,7 +31,7 @@ export function useValidation< T >(
return {
ref,
- error: context.errors[ validatorId ],
+ error: context.errors[ validatorId ]?.message,
isValidating,
async validate( newData?: Record< string, unknown > ) {
setIsValidating( true );
diff --git a/packages/js/product-editor/src/contexts/validation-context/use-validations.ts b/packages/js/product-editor/src/contexts/validation-context/use-validations.ts
index b48222e02d5..affa3590841 100644
--- a/packages/js/product-editor/src/contexts/validation-context/use-validations.ts
+++ b/packages/js/product-editor/src/contexts/validation-context/use-validations.ts
@@ -17,6 +17,29 @@ export function useValidations< T = unknown >() {
const context = useContext( ValidationContext );
const [ isValidating, setIsValidating ] = useState( false );
+ async function focusByValidatorId( validatorId: string ) {
+ const field = await context.getFieldByValidatorId( validatorId );
+
+ if ( ! field ) {
+ return;
+ }
+ const tab = field.closest(
+ '.wp-block-woocommerce-product-tab__content'
+ );
+ const observer = new MutationObserver( () => {
+ if ( tab && getComputedStyle( tab ).display !== 'none' ) {
+ field.focus();
+ observer.disconnect();
+ }
+ } );
+
+ if ( tab ) {
+ observer.observe( tab, {
+ attributes: true,
+ } );
+ }
+ }
+
return {
isValidating,
async validate( newData?: Partial< T > ) {
@@ -38,5 +61,6 @@ export function useValidations< T = unknown >() {
setIsValidating( false );
} );
},
+ focusByValidatorId,
};
}
diff --git a/packages/js/product-editor/src/contexts/validation-context/validation-context.ts b/packages/js/product-editor/src/contexts/validation-context/validation-context.ts
index 7cfe64d0f02..b70036e3b23 100644
--- a/packages/js/product-editor/src/contexts/validation-context/validation-context.ts
+++ b/packages/js/product-editor/src/contexts/validation-context/validation-context.ts
@@ -12,6 +12,7 @@ import { ValidationContextProps } from './types';
export const ValidationContext = createContext< ValidationContextProps< any > >(
{
errors: {},
+ getFieldByValidatorId: () => ( {} as Promise< HTMLElement > ),
registerValidator: () => () => {},
unRegisterValidator: () => () => {},
validateField: () => Promise.resolve( undefined ),
diff --git a/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx b/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx
index de6e2bad218..393fa7e17bf 100644
--- a/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx
+++ b/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx
@@ -9,6 +9,7 @@ import { createElement, useRef, useState } from '@wordpress/element';
* Internal dependencies
*/
import {
+ ValidationError,
ValidationErrors,
ValidationProviderProps,
Validator,
@@ -64,17 +65,25 @@ export function ValidationProvider< T >( {
const result = validator( initialValue, newData );
return result.then( ( error ) => {
+ const errorWithValidatorId: ValidationError =
+ error !== undefined ? { validatorId, ...error } : undefined;
setErrors( ( currentErrors ) => ( {
...currentErrors,
- [ validatorId ]: error,
+ [ validatorId ]: errorWithValidatorId,
} ) );
- return error;
+ return errorWithValidatorId;
} );
}
return Promise.resolve( undefined );
}
+ async function getFieldByValidatorId(
+ validatorId: string
+ ): Promise< HTMLElement > {
+ return fieldRefs.current[ validatorId ];
+ }
+
async function validateAll(
newData: Partial< T >
): Promise< ValidationErrors > {
@@ -104,6 +113,7 @@ export function ValidationProvider< T >( {
( {
+ select: jest.fn( () => ( {
+ getBlockParentsByBlockName: mockGetBlockParentsByBlockName,
+ getBlock: mockGetBlock,
+ } ) ),
+} ) );
+
+describe( 'useBlocksHelper', () => {
+ beforeEach( () => {
+ jest.clearAllMocks();
+ } );
+
+ it( 'should return the closest parent tab id', () => {
+ const clientId = 'test-client-id';
+ const parentClientId = 'parent-client-id';
+ const attributes = { id: 'parent-tab-id' };
+
+ mockGetBlockParentsByBlockName.mockReturnValue( [ parentClientId ] );
+ mockGetBlock.mockReturnValue( { attributes } );
+
+ const { result } = renderHook( () => useBlocksHelper() );
+ const { getParentTabId } = result.current;
+
+ const tabId = getParentTabId( clientId );
+
+ expect( tabId ).toBe( 'parent-tab-id' );
+ expect( mockGetBlockParentsByBlockName ).toHaveBeenCalledWith(
+ clientId,
+ 'woocommerce/product-tab',
+ true
+ );
+ expect( mockGetBlock ).toHaveBeenCalledWith( parentClientId );
+ } );
+
+ it( 'should return null if no parent tab id is found', () => {
+ const clientId = 'test-client-id';
+
+ mockGetBlockParentsByBlockName.mockReturnValue( [] );
+
+ const { result } = renderHook( () => useBlocksHelper() );
+ const { getParentTabId } = result.current;
+
+ const tabId = getParentTabId( clientId );
+
+ expect( tabId ).toBe( null );
+ expect( mockGetBlockParentsByBlockName ).toHaveBeenCalledWith(
+ clientId,
+ 'woocommerce/product-tab',
+ true
+ );
+ expect( mockGetBlock ).not.toHaveBeenCalled();
+ } );
+
+ it( 'should return `undefined` if parent block has no attributes', () => {
+ const clientId = 'test-client-id';
+ const parentClientId = 'parent-client-id';
+
+ mockGetBlockParentsByBlockName.mockReturnValue( [ parentClientId ] );
+ mockGetBlock.mockReturnValue( {} );
+
+ const { result } = renderHook( () => useBlocksHelper() );
+ const { getParentTabId } = result.current;
+
+ const tabId = getParentTabId( clientId );
+
+ expect( tabId ).toBeUndefined();
+ expect( mockGetBlockParentsByBlockName ).toHaveBeenCalledWith(
+ clientId,
+ 'woocommerce/product-tab',
+ true
+ );
+ expect( mockGetBlock ).toHaveBeenCalledWith( parentClientId );
+ } );
+} );
diff --git a/packages/js/product-editor/src/hooks/test/use-error-handler.test.ts b/packages/js/product-editor/src/hooks/test/use-error-handler.test.ts
new file mode 100644
index 00000000000..79599a0f384
--- /dev/null
+++ b/packages/js/product-editor/src/hooks/test/use-error-handler.test.ts
@@ -0,0 +1,190 @@
+/**
+ * External dependencies
+ */
+import { renderHook } from '@testing-library/react-hooks';
+
+/**
+ * Internal dependencies
+ */
+import { useErrorHandler, WPError } from '../use-error-handler';
+import { useBlocksHelper } from '../use-blocks-helper';
+
+const mockNavigateTo = jest.fn();
+const mockFocusByValidatorId = jest.fn();
+
+jest.mock( '@woocommerce/navigation', () => ( {
+ getNewPath: jest.fn().mockReturnValue( '/new-path' ),
+ navigateTo: jest.fn( ( args ) => mockNavigateTo( args ) ),
+} ) );
+
+jest.mock( '@wordpress/i18n', () => ( {
+ __: jest.fn( ( msg ) => msg ),
+} ) );
+
+jest.mock( '../../contexts/validation-context', () => ( {
+ useValidations: jest.fn().mockReturnValue( {
+ focusByValidatorId: jest.fn( ( args ) =>
+ mockFocusByValidatorId( args )
+ ),
+ } ),
+} ) );
+
+jest.mock( '@wordpress/data', () => ( {
+ select: jest.fn().mockReturnValue( {
+ getBlockParentsByBlockName: jest.fn().mockReturnValue( [ 'parent' ] ),
+ } ),
+} ) );
+
+jest.mock( '../use-blocks-helper', () => ( {
+ useBlocksHelper: jest.fn().mockReturnValue( {
+ getParentTabId: jest.fn( () => 'inventory' ),
+ getParentTabIdByBlockName: jest.fn( () => 'inventory' ),
+ } ),
+} ) );
+
+describe( 'useErrorHandler', () => {
+ beforeEach( () => {
+ jest.clearAllMocks();
+ } );
+
+ it( 'should return the correct error message and props when exists and the field is visible', () => {
+ const error = {
+ code: 'product_invalid_sku',
+ message: 'Invalid or duplicated SKU.',
+ } as WPError;
+ const visibleTab = 'inventory';
+
+ const { result } = renderHook( () => useErrorHandler() );
+ const { getProductErrorMessageAndProps } = result.current;
+
+ const { message, errorProps } = getProductErrorMessageAndProps(
+ error,
+ visibleTab
+ );
+
+ expect( message ).toBe( 'Invalid or duplicated SKU.' );
+ expect( errorProps ).toEqual( {} );
+ } );
+
+ it( 'should return the correct error message and props when exists and the field is not visible', () => {
+ const error = {
+ code: 'product_invalid_sku',
+ } as WPError;
+ const visibleTab = 'general';
+
+ const { result } = renderHook( () => useErrorHandler() );
+ const { getProductErrorMessageAndProps } = result.current;
+
+ const { message, errorProps } = getProductErrorMessageAndProps(
+ error,
+ visibleTab
+ );
+
+ expect( message ).toBe( 'Invalid or duplicated SKU.' );
+ expect( errorProps.explicitDismiss ).toBeTruthy();
+ } );
+
+ it( 'should call focusByValidatorId for form field errors when errorProps action is triggered', () => {
+ const error = {
+ code: 'product_form_field_error',
+ validatorId: 'test-validator',
+ } as WPError;
+ const visibleTab = 'general';
+
+ const { result } = renderHook( () => useErrorHandler() );
+ const { getProductErrorMessageAndProps } = result.current;
+
+ const { errorProps } = getProductErrorMessageAndProps(
+ error,
+ visibleTab
+ );
+
+ expect( errorProps ).toBeDefined();
+ expect( errorProps.actions ).toBeDefined();
+ expect( errorProps.actions?.length ).toBeGreaterThan( 0 );
+
+ // Trigger the action
+ if ( errorProps.actions && errorProps.actions.length > 0 ) {
+ errorProps.actions[ 0 ].onClick();
+ }
+
+ expect( mockFocusByValidatorId ).toHaveBeenCalledWith(
+ 'test-validator'
+ );
+ } );
+ it( 'should call getParentTabIdByBlockName and focusByValidatorId for invalid sku errors when errorProps action is triggered', () => {
+ const error = {
+ code: 'product_invalid_sku',
+ } as WPError;
+ const visibleTab = 'general';
+
+ const { result } = renderHook( () => useErrorHandler() );
+ const { getProductErrorMessageAndProps } = result.current;
+
+ const { errorProps: fieldsErrorProps } = getProductErrorMessageAndProps(
+ error,
+ visibleTab
+ );
+
+ expect( fieldsErrorProps ).toBeDefined();
+ expect( fieldsErrorProps.actions ).toBeDefined();
+ expect( fieldsErrorProps.actions?.length ).toBeGreaterThan( 0 );
+
+ // Trigger the action
+ if ( fieldsErrorProps.actions && fieldsErrorProps.actions.length > 0 ) {
+ fieldsErrorProps.actions[ 0 ].onClick();
+ }
+
+ expect( mockFocusByValidatorId ).toHaveBeenCalledWith( 'sku' );
+ } );
+ it( 'should not call getErrorPropsWithActions for invalid sku errors when getParentTabIdByBlockName returns null', () => {
+ const error = {
+ code: 'product_invalid_sku',
+ } as WPError;
+ const visibleTab = 'general';
+
+ ( useBlocksHelper as jest.Mock ).mockReturnValue( {
+ getParentTabId: jest.fn( () => null ), // Mock returns null
+ getParentTabIdByBlockName: jest.fn( () => null ), // Mock returns null
+ } );
+
+ const { result } = renderHook( () => useErrorHandler() );
+ const { getProductErrorMessageAndProps } = result.current;
+
+ const { message, errorProps } = getProductErrorMessageAndProps(
+ error,
+ visibleTab
+ );
+
+ expect( errorProps ).toBeDefined();
+ expect( errorProps.actions ).not.toBeDefined();
+ expect( mockFocusByValidatorId ).not.toHaveBeenCalled();
+ expect( message ).toBe( 'Invalid or duplicated SKU.' );
+ } );
+ it( 'should not call getErrorPropsWithActions for form field errors when getParentTabId returns null', () => {
+ const error = {
+ code: 'product_form_field_error',
+ validatorId: 'test-validator',
+ message: 'Test error message',
+ } as WPError;
+ const visibleTab = 'inventory';
+
+ ( useBlocksHelper as jest.Mock ).mockReturnValue( {
+ getParentTabId: jest.fn( () => null ), // Mock returns null
+ getParentTabIdByBlockName: jest.fn( () => null ), // Mock returns null
+ } );
+
+ const { result } = renderHook( () => useErrorHandler() );
+ const { getProductErrorMessageAndProps } = result.current;
+
+ const { message, errorProps } = getProductErrorMessageAndProps(
+ error,
+ visibleTab
+ );
+
+ expect( errorProps ).toBeDefined();
+ expect( errorProps.actions ).not.toBeDefined();
+ expect( mockFocusByValidatorId ).not.toHaveBeenCalled();
+ expect( message ).toBe( 'Test error message' );
+ } );
+} );
diff --git a/packages/js/product-editor/src/hooks/use-blocks-helper/index.ts b/packages/js/product-editor/src/hooks/use-blocks-helper/index.ts
new file mode 100644
index 00000000000..5ad5df55bcf
--- /dev/null
+++ b/packages/js/product-editor/src/hooks/use-blocks-helper/index.ts
@@ -0,0 +1 @@
+export * from './use-blocks-helper';
diff --git a/packages/js/product-editor/src/hooks/use-blocks-helper/use-blocks-helper.ts b/packages/js/product-editor/src/hooks/use-blocks-helper/use-blocks-helper.ts
new file mode 100644
index 00000000000..b1522746825
--- /dev/null
+++ b/packages/js/product-editor/src/hooks/use-blocks-helper/use-blocks-helper.ts
@@ -0,0 +1,47 @@
+/**
+ * External dependencies
+ */
+import { select } from '@wordpress/data';
+
+export function useBlocksHelper() {
+ function getClosestParentTabId( clientId: string ) {
+ const [ closestParentClientId ] =
+ // @ts-expect-error Outdated type definition.
+ select( 'core/block-editor' ).getBlockParentsByBlockName(
+ clientId,
+ 'woocommerce/product-tab',
+ true
+ );
+ if ( ! closestParentClientId ) {
+ return null;
+ }
+ // @ts-expect-error Outdated type definition.
+ const { attributes } = select( 'core/block-editor' ).getBlock(
+ closestParentClientId
+ );
+ return attributes?.id;
+ }
+
+ function getParentTabId( clientId?: string ) {
+ if ( clientId ) {
+ return getClosestParentTabId( clientId );
+ }
+ return null;
+ }
+
+ function getParentTabIdByBlockName( blockName: string ) {
+ const blockClientIds =
+ // @ts-expect-error Outdated type definition.
+ select( 'core/block-editor' ).getBlocksByName( blockName );
+
+ if ( blockClientIds.length ) {
+ return getClosestParentTabId( blockClientIds[ 0 ] );
+ }
+ return null;
+ }
+
+ return {
+ getParentTabId,
+ getParentTabIdByBlockName,
+ };
+}
diff --git a/packages/js/product-editor/src/hooks/use-currency-input-props.ts b/packages/js/product-editor/src/hooks/use-currency-input-props.ts
index d3716ed1ccc..20647703622 100644
--- a/packages/js/product-editor/src/hooks/use-currency-input-props.ts
+++ b/packages/js/product-editor/src/hooks/use-currency-input-props.ts
@@ -27,6 +27,8 @@ type Props = {
onKeyUp?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
};
+const CURRENCY_INPUT_MAX = 1_000_000_000_000_000_000.0;
+
export const useCurrencyInputProps = ( {
value,
onChange,
@@ -72,7 +74,11 @@ export const useCurrencyInputProps = ( {
onChange( newValue: string ) {
const sanitizeValue = sanitizePrice( newValue );
if ( onChange ) {
- onChange( sanitizeValue );
+ onChange(
+ Number( sanitizeValue ) <= CURRENCY_INPUT_MAX
+ ? sanitizeValue
+ : String( CURRENCY_INPUT_MAX )
+ );
}
},
};
diff --git a/packages/js/product-editor/src/hooks/use-error-handler.ts b/packages/js/product-editor/src/hooks/use-error-handler.ts
new file mode 100644
index 00000000000..7adb4bd71c1
--- /dev/null
+++ b/packages/js/product-editor/src/hooks/use-error-handler.ts
@@ -0,0 +1,186 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useCallback } from '@wordpress/element';
+import { getNewPath, navigateTo } from '@woocommerce/navigation';
+
+/**
+ * Internal dependencies
+ */
+import { useValidations } from '../contexts/validation-context';
+import { useBlocksHelper } from './use-blocks-helper';
+
+export type WPErrorCode =
+ | 'variable_product_no_variation_prices'
+ | 'product_form_field_error'
+ | 'product_invalid_sku'
+ | 'product_invalid_global_unique_id'
+ | 'product_create_error'
+ | 'product_publish_error'
+ | 'product_preview_error';
+
+export type WPError = {
+ code: WPErrorCode;
+ message: string;
+ validatorId?: string;
+ context?: string;
+};
+
+type ErrorProps = {
+ explicitDismiss: boolean;
+ actions?: ErrorAction[];
+};
+
+type ErrorAction = {
+ label: string;
+ onClick: () => void;
+};
+
+type UseErrorHandlerTypes = {
+ getProductErrorMessageAndProps: (
+ error: WPError,
+ visibleTab: string | null
+ ) => {
+ message: string;
+ errorProps: ErrorProps;
+ };
+};
+
+function getUrl( tab: string ): string {
+ return getNewPath( { tab } );
+}
+
+function getErrorPropsWithActions(
+ errorContext = '',
+ validatorId: string,
+ focusByValidatorId: ( validatorId: string ) => void,
+ label: string = __( 'View error', 'woocommerce' )
+): ErrorProps {
+ return {
+ explicitDismiss: true,
+ actions: [
+ {
+ label,
+ onClick: async () => {
+ await focusByValidatorId( validatorId );
+ navigateTo( {
+ url: getUrl( errorContext ),
+ } );
+ },
+ },
+ ],
+ };
+}
+
+export const useErrorHandler = (): UseErrorHandlerTypes => {
+ const { focusByValidatorId } = useValidations();
+ const { getParentTabId, getParentTabIdByBlockName } = useBlocksHelper();
+
+ const getProductErrorMessageAndProps = useCallback(
+ ( error: WPError, visibleTab: string | null ) => {
+ const response = {
+ message: '',
+ errorProps: {} as ErrorProps,
+ };
+ const {
+ code,
+ context = '',
+ message: errorMessage,
+ validatorId = '',
+ } = error;
+ const errorContext = getParentTabId( context );
+ switch ( code ) {
+ case 'variable_product_no_variation_prices':
+ response.message = errorMessage;
+ if (
+ visibleTab !== 'variations' &&
+ errorContext !== null
+ ) {
+ response.errorProps = getErrorPropsWithActions(
+ errorContext,
+ validatorId,
+ focusByValidatorId
+ );
+ }
+ break;
+ case 'product_form_field_error':
+ response.message = errorMessage;
+ if (
+ visibleTab !== errorContext &&
+ errorContext !== null
+ ) {
+ response.errorProps = getErrorPropsWithActions(
+ errorContext,
+ validatorId,
+ focusByValidatorId
+ );
+ }
+ break;
+ case 'product_invalid_sku':
+ response.message = __(
+ 'Invalid or duplicated SKU.',
+ 'woocommerce'
+ );
+ const errorSkuContext = getParentTabIdByBlockName(
+ 'woocommerce/product-sku-field'
+ );
+ if (
+ visibleTab !== errorSkuContext &&
+ errorSkuContext !== null
+ ) {
+ response.errorProps = getErrorPropsWithActions(
+ errorSkuContext,
+ 'sku',
+ focusByValidatorId,
+ __( 'View SKU field', 'woocommerce' )
+ );
+ }
+ break;
+ case 'product_invalid_global_unique_id':
+ response.message = __(
+ 'Invalid or duplicated GTIN, UPC, EAN or ISBN.',
+ 'woocommerce'
+ );
+ const errorUniqueIdContext = errorContext || 'inventory';
+ if ( visibleTab !== errorUniqueIdContext ) {
+ response.errorProps = getErrorPropsWithActions(
+ errorUniqueIdContext,
+ 'global_unique_id',
+ focusByValidatorId,
+ __( 'View identifier field', 'woocommerce' )
+ );
+ }
+ break;
+ case 'product_create_error':
+ response.message = __(
+ 'Failed to create product.',
+ 'woocommerce'
+ );
+ break;
+ case 'product_publish_error':
+ response.message = __(
+ 'Failed to publish product.',
+ 'woocommerce'
+ );
+ break;
+ case 'product_preview_error':
+ response.message = __(
+ 'Failed to preview product.',
+ 'woocommerce'
+ );
+ break;
+ default:
+ response.message = __(
+ 'Failed to save product.',
+ 'woocommerce'
+ );
+ break;
+ }
+ return response;
+ },
+ []
+ );
+
+ return { getProductErrorMessageAndProps };
+};
diff --git a/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts b/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts
index 6bc0ec00fc6..584bdc3fae7 100644
--- a/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts
+++ b/packages/js/product-editor/src/hooks/use-product-manager/use-product-manager.ts
@@ -10,7 +10,7 @@ import { Product, ProductStatus, PRODUCTS_STORE_NAME } from '@woocommerce/data';
* Internal dependencies
*/
import { useValidations } from '../../contexts/validation-context';
-import type { WPError } from '../../utils/get-product-error-message-and-props';
+import type { WPError } from '../../hooks/use-error-handler';
import { AUTO_DRAFT_NAME } from '../../utils/constants';
export function errorHandler( error: WPError, productStatus: ProductStatus ) {
@@ -18,21 +18,21 @@ export function errorHandler( error: WPError, productStatus: ProductStatus ) {
return error;
}
+ const errorObj = Object.values( error ).find(
+ ( value ) => value !== undefined
+ ) as WPError | undefined;
+
if ( 'variations' in error && error.variations ) {
return {
+ ...errorObj,
code: 'variable_product_no_variation_prices',
- message: error.variations,
};
}
- const errorMessage = Object.values( error ).find(
- ( value ) => value !== undefined
- ) as string | undefined;
-
- if ( errorMessage !== undefined ) {
+ if ( errorObj !== undefined ) {
return {
+ ...errorObj,
code: 'product_form_field_error',
- message: errorMessage,
};
}
diff --git a/packages/js/product-editor/src/style.scss b/packages/js/product-editor/src/style.scss
index e4de003e389..ce54a3a1c3a 100644
--- a/packages/js/product-editor/src/style.scss
+++ b/packages/js/product-editor/src/style.scss
@@ -52,6 +52,7 @@
@import "components/attribute-combobox-field/styles.scss";
@import "components/number-control/style.scss";
@import "components/empty-state/style.scss";
+@import "components/combobox-control/style.scss";
/* Field Blocks */
diff --git a/packages/js/product-editor/src/utils/get-product-error-message-and-props.ts b/packages/js/product-editor/src/utils/get-product-error-message-and-props.ts
deleted file mode 100644
index 6d86db93137..00000000000
--- a/packages/js/product-editor/src/utils/get-product-error-message-and-props.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * External dependencies
- */
-import { __ } from '@wordpress/i18n';
-
-export type WPErrorCode =
- | 'variable_product_no_variation_prices'
- | 'product_form_field_error'
- | 'product_invalid_sku'
- | 'product_invalid_global_unique_id'
- | 'product_create_error'
- | 'product_publish_error'
- | 'product_preview_error';
-
-export type WPError = {
- code: WPErrorCode;
- message: string;
- data: {
- [ key: string ]: unknown;
- };
-};
-
-type ErrorProps = {
- explicitDismiss: boolean;
-};
-
-export function getProductErrorMessageAndProps(
- error: WPError,
- visibleTab: string | null
-): {
- message: string;
- errorProps: ErrorProps;
-} {
- const response = {
- message: '',
- errorProps: {} as ErrorProps,
- };
- switch ( error.code ) {
- case 'variable_product_no_variation_prices':
- response.message = error.message;
- if ( visibleTab !== 'variations' ) {
- response.errorProps = { explicitDismiss: true };
- }
- break;
- case 'product_form_field_error':
- response.message = error.message;
- if ( visibleTab !== 'general' ) {
- response.errorProps = { explicitDismiss: true };
- }
- break;
- case 'product_invalid_sku':
- response.message = __(
- 'Invalid or duplicated SKU.',
- 'woocommerce'
- );
- if ( visibleTab !== 'inventory' ) {
- response.errorProps = { explicitDismiss: true };
- }
- break;
- case 'product_invalid_global_unique_id':
- response.message = __(
- 'Invalid or duplicated GTIN, UPC, EAN or ISBN.',
- 'woocommerce'
- );
- if ( visibleTab !== 'inventory' ) {
- response.errorProps = { explicitDismiss: true };
- }
- break;
- case 'product_create_error':
- response.message = __( 'Failed to create product.', 'woocommerce' );
- break;
- case 'product_publish_error':
- response.message = __(
- 'Failed to publish product.',
- 'woocommerce'
- );
- break;
- case 'product_preview_error':
- response.message = __(
- 'Failed to preview product.',
- 'woocommerce'
- );
- break;
- default:
- response.message = __( 'Failed to save product.', 'woocommerce' );
- break;
- }
- return response;
-}
diff --git a/packages/js/product-editor/src/utils/sanitize-html.ts b/packages/js/product-editor/src/utils/sanitize-html.ts
index 083a70bd874..cf0166dc656 100644
--- a/packages/js/product-editor/src/utils/sanitize-html.ts
+++ b/packages/js/product-editor/src/utils/sanitize-html.ts
@@ -3,8 +3,8 @@
*/
import { sanitize } from 'dompurify';
-const ALLOWED_TAGS = [ 'a', 'b', 'em', 'i', 'strong', 'p', 'br' ];
-const ALLOWED_ATTR = [ 'target', 'href', 'rel', 'name', 'download' ];
+const ALLOWED_TAGS = [ 'a', 'b', 'em', 'i', 'strong', 'p', 'br', 'abbr' ];
+const ALLOWED_ATTR = [ 'target', 'href', 'rel', 'name', 'download', 'title' ];
export function sanitizeHTML(
html: string,
diff --git a/packages/js/product-editor/src/utils/test/get-product-error-message.ts b/packages/js/product-editor/src/utils/test/get-product-error-message.ts
deleted file mode 100644
index 406cbba7007..00000000000
--- a/packages/js/product-editor/src/utils/test/get-product-error-message.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * Internal dependencies
- */
-import {
- getProductErrorMessageAndProps,
- WPError,
-} from '../get-product-error-message-and-props';
-
-describe( 'getProductErrorMessageAndProps.message', () => {
- it( 'should return the correct error message and props when exists and the field is visible', () => {
- const error = {
- code: 'product_invalid_sku',
- } as WPError;
- const visibleTab = 'inventory';
- const { message, errorProps } = getProductErrorMessageAndProps(
- error,
- visibleTab
- );
- expect( message ).toBe( 'Invalid or duplicated SKU.' );
- expect( errorProps.explicitDismiss ).toBeFalsy();
- } );
-
- it( 'should return the correct error message and props when exists and the field is not visible', () => {
- const error = {
- code: 'product_invalid_sku',
- } as WPError;
- const visibleTab = 'general';
- const { message, errorProps } = getProductErrorMessageAndProps(
- error,
- visibleTab
- );
- expect( message ).toBe( 'Invalid or duplicated SKU.' );
- expect( errorProps.explicitDismiss ).toBeTruthy();
- } );
-
- it( 'should return a default message and props when the error code is not mapped', () => {
- const error = {} as WPError;
- const visibleTab = 'general';
- const { message, errorProps } = getProductErrorMessageAndProps(
- error,
- visibleTab
- );
- expect( message ).toBe( 'Failed to save product.' );
- expect( errorProps.explicitDismiss ).toBeFalsy();
- } );
-} );
diff --git a/packages/php/remote-specs-validation/changelog/47614-tweak-google-extension-rename b/packages/php/remote-specs-validation/changelog/47614-tweak-google-extension-rename
new file mode 100644
index 00000000000..4ac5ec3e8d9
--- /dev/null
+++ b/packages/php/remote-specs-validation/changelog/47614-tweak-google-extension-rename
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Rename Google Listings and Ads with Google for WooCommerce #### Comment
\ No newline at end of file
diff --git a/packages/php/remote-specs-validation/changelog/tweak-google-extension-rename b/packages/php/remote-specs-validation/changelog/tweak-google-extension-rename
new file mode 100644
index 00000000000..8a85377cbb9
--- /dev/null
+++ b/packages/php/remote-specs-validation/changelog/tweak-google-extension-rename
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Rename Google Listings and Ads with Google for WooCommerce
diff --git a/packages/php/remote-specs-validation/tests/fixtures/obw-free-extensions.json b/packages/php/remote-specs-validation/tests/fixtures/obw-free-extensions.json
index 4147a07ab43..416dc84228e 100644
--- a/packages/php/remote-specs-validation/tests/fixtures/obw-free-extensions.json
+++ b/packages/php/remote-specs-validation/tests/fixtures/obw-free-extensions.json
@@ -425,8 +425,8 @@
"key": "mailpoet"
},
{
- "name": "Google Listings & Ads",
- "description": "Drive sales with Google Listings and Ads ",
+ "name": "Google for WooCommerce",
+ "description": "Drive sales with Google for WooCommerce ",
"image_url": "https://woocommerce.com/wp-content/plugins/wccom-plugins/obw-free-extensions/images/google.svg",
"manage_url": "admin.php?page=wc-admin&path=%2Fgoogle%2Fstart",
"is_visible": [
@@ -501,7 +501,7 @@
"title": "Grow your store",
"plugins": [
{
- "name": "Google Listings & Ads",
+ "name": "Google for WooCommerce",
"description": "Reach more shoppers and drive sales for your store. Integrate with Google to list your products for free and launch paid ad campaigns.",
"image_url": "https://woocommerce.com/wp-content/plugins/wccom-plugins/obw-free-extensions/images/google.svg",
"manage_url": "admin.php?page=wc-admin&path=%2Fgoogle%2Fstart",
@@ -1034,7 +1034,7 @@
"install_priority": 7
},
{
- "name": "Google Listings & Ads",
+ "name": "Google for WooCommerce",
"description": "Reach millions of active shoppers across Google with free product listings and ads.",
"image_url": "https://woocommerce.com/wp-content/plugins/wccom-plugins/obw-free-extensions/images/core-profiler/logo-google.svg",
"manage_url": "admin.php?page=wc-admin&path=%2Fgoogle%2Fstart",
@@ -1042,7 +1042,7 @@
"is_built_by_wc": true,
"min_php_version": "7.4",
"key": "google-listings-and-ads",
- "label": "Drive sales with Google Listings & Ads",
+ "label": "Drive sales with Google for WooCommerce",
"learn_more_link": "https://woocommerce.com/products/google-listings-and-ads",
"install_priority": 6
},
diff --git a/plugins/woo-ai/bin/build-zip.sh b/plugins/woo-ai/bin/build-zip.sh
index 58f3b85d114..012e0a422f0 100755
--- a/plugins/woo-ai/bin/build-zip.sh
+++ b/plugins/woo-ai/bin/build-zip.sh
@@ -10,7 +10,7 @@ rm -rf "$BUILD_PATH"
mkdir -p "$DEST_PATH"
echo "Installing PHP and JS dependencies..."
-pnpm install
+pnpm install --frozen-lockfile
echo "Running JS Build..."
pnpm --filter='@woocommerce/plugin-woo-ai' build || exit "$?"
diff --git a/plugins/woo-ai/changelog/49310-dev-pin-block-env-package b/plugins/woo-ai/changelog/49310-dev-pin-block-env-package
new file mode 100644
index 00000000000..0802125e226
--- /dev/null
+++ b/plugins/woo-ai/changelog/49310-dev-pin-block-env-package
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+Comment: bump wp-env to 9.7.0, include blocks in syncpack
+
diff --git a/plugins/woo-ai/changelog/dev-babel-loader-jest-caching b/plugins/woo-ai/changelog/dev-babel-loader-jest-caching
new file mode 100644
index 00000000000..49889afff8e
--- /dev/null
+++ b/plugins/woo-ai/changelog/dev-babel-loader-jest-caching
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Monorepo: enable Jest caching.
diff --git a/plugins/woo-ai/changelog/dev-try-faster-building-zip b/plugins/woo-ai/changelog/dev-try-faster-building-zip
new file mode 100644
index 00000000000..310400b9388
--- /dev/null
+++ b/plugins/woo-ai/changelog/dev-try-faster-building-zip
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Monorepo: minor tweaks in zip building script (use frozen lock file when installing dependecies).
diff --git a/plugins/woo-ai/changelog/dev-webpack-loaders-scannig-paths-tweaks b/plugins/woo-ai/changelog/dev-webpack-loaders-scannig-paths-tweaks
new file mode 100644
index 00000000000..30f765e3fca
--- /dev/null
+++ b/plugins/woo-ai/changelog/dev-webpack-loaders-scannig-paths-tweaks
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Monorepo: tweak Webpack loaders paths filtering for better build perfromance.
diff --git a/plugins/woo-ai/package.json b/plugins/woo-ai/package.json
index c7f29f8585a..e42679122a2 100644
--- a/plugins/woo-ai/package.json
+++ b/plugins/woo-ai/package.json
@@ -27,7 +27,7 @@
"@woocommerce/dependency-extraction-webpack-plugin": "workspace:*",
"@woocommerce/eslint-plugin": "workspace:*",
"@wordpress/data": "wp-6.0",
- "@wordpress/env": "^9.0.7",
+ "@wordpress/env": "^9.7.0",
"@wordpress/prettier-config": "2.17.0",
"@wordpress/scripts": "^19.2.4",
"babel-jest": "~27.5.1",
diff --git a/plugins/woo-ai/tests/js/jest.config.js b/plugins/woo-ai/tests/js/jest.config.js
index 5314da3e1d7..89c316038a5 100644
--- a/plugins/woo-ai/tests/js/jest.config.js
+++ b/plugins/woo-ai/tests/js/jest.config.js
@@ -17,6 +17,7 @@ module.exports = {
'/node_modules/',
'/build/',
'/.*/build/',
+ '/vendor',
'/tests',
],
transformIgnorePatterns: [
diff --git a/plugins/woo-ai/webpack.config.js b/plugins/woo-ai/webpack.config.js
index d131f0792b0..24355a5325a 100644
--- a/plugins/woo-ai/webpack.config.js
+++ b/plugins/woo-ai/webpack.config.js
@@ -1,5 +1,6 @@
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
const WooCommerceDependencyExtractionWebpackPlugin = require( '@woocommerce/dependency-extraction-webpack-plugin' );
+const path = require( 'path' );
module.exports = {
...defaultConfig,
@@ -13,7 +14,7 @@ module.exports = {
{
test: /\.tsx?$/,
use: 'ts-loader',
- exclude: /node_modules/,
+ include: [ path.resolve( __dirname, './src/' ) ],
},
{
test: /\.(png|jp(e*)g|svg|gif)$/,
diff --git a/plugins/woocommerce-admin/bin/modified-editsite-lock-unlock.js b/plugins/woocommerce-admin/bin/modified-editsite-lock-unlock.js
deleted file mode 100644
index 140c67f8930..00000000000
--- a/plugins/woocommerce-admin/bin/modified-editsite-lock-unlock.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * External dependencies
- */
-import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
-
-const wordPressConsentString = {
- 6.4: 'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.',
- 6.5: 'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.',
- 6.6: 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
-};
-
-function optInToUnstableAPIs() {
- let error;
- for ( const optInString of Object.values( wordPressConsentString ) ) {
- try {
- return __dangerousOptInToUnstableAPIsOnlyForCoreModules(
- optInString,
- '@wordpress/edit-site'
- );
- } catch ( anError ) {
- error = anError;
- }
- }
-
- throw error;
-}
-
-export const { lock, unlock } = optInToUnstableAPIs();
-//# sourceMappingURL=lock-unlock.js.map
diff --git a/plugins/woocommerce-admin/client/activity-panel/panels/help.js b/plugins/woocommerce-admin/client/activity-panel/panels/help.js
index 4f4251c334e..2d29a5babb8 100644
--- a/plugins/woocommerce-admin/client/activity-panel/panels/help.js
+++ b/plugins/woocommerce-admin/client/activity-panel/panels/help.js
@@ -87,7 +87,7 @@ function getMarketingItems( props ) {
link: 'https://kb.mailpoet.com/category/114-getting-started',
},
activePlugins.includes( 'google-listings-and-ads' ) && {
- title: __( 'Set up Google Listing & Ads', 'woocommerce' ),
+ title: __( 'Set up Google for WooCommerce', 'woocommerce' ),
link: 'https://woocommerce.com/document/google-listings-and-ads/?utm_medium=product#get-started',
},
activePlugins.includes( 'pinterest-for-woocommerce' ) && {
diff --git a/plugins/woocommerce-admin/client/analytics/report/customers/table.js b/plugins/woocommerce-admin/client/analytics/report/customers/table.js
index 874d70108b7..d3f77850281 100644
--- a/plugins/woocommerce-admin/client/analytics/report/customers/table.js
+++ b/plugins/woocommerce-admin/client/analytics/report/customers/table.js
@@ -5,7 +5,7 @@ import { __, _n } from '@wordpress/i18n';
import { Fragment, useContext } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { Tooltip } from '@wordpress/components';
-import { Date, Link } from '@woocommerce/components';
+import { Date, Link, Pill } from '@woocommerce/components';
import { formatValue } from '@woocommerce/number';
import { getAdminLink } from '@woocommerce/settings';
import { defaultTableDateFormat } from '@woocommerce/date';
@@ -141,6 +141,12 @@ function CustomersReportTable( {
country,
} = customer;
const countryName = getCountryName( country );
+ const customerName =
+ name?.trim() !== '' ? (
+ name
+ ) : (
+ { __( 'Guest', 'woocommerce' ) }
+ );
const customerNameLink = userId ? (
) : (
- name
+ customerName
);
const dateLastActiveDisplay = dateLastActive ? (
diff --git a/plugins/woocommerce-admin/client/core-profiler/index.tsx b/plugins/woocommerce-admin/client/core-profiler/index.tsx
index 02cce7e4abf..ede4bd2b55a 100644
--- a/plugins/woocommerce-admin/client/core-profiler/index.tsx
+++ b/plugins/woocommerce-admin/client/core-profiler/index.tsx
@@ -260,9 +260,14 @@ const assignCurrentUserEmail = assign( {
const assignOnboardingProfile = assign( {
onboardingProfile: ( {
event,
+ context,
}: {
event: DoneActorEvent< OnboardingProfile | undefined >;
- } ) => event.output,
+ context: CoreProfilerStateMachineContext;
+ } ) =>
+ ! event.output || typeof event.output !== 'object'
+ ? context.onboardingProfile // if the onboarding profile is not an object, keep the existing context
+ : event.output,
} );
const getGeolocation = fromPromise(
diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx
index aff41d5d3b9..8a682cee24a 100644
--- a/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx
+++ b/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx
@@ -124,12 +124,12 @@ export const BusinessInfo = ( {
businessInfo,
countries,
onboardingProfile: {
- is_store_country_set: isStoreCountrySet,
- industry: industryFromOnboardingProfile,
- business_choice: businessChoiceFromOnboardingProfile,
- is_agree_marketing: isOptInMarketingFromOnboardingProfile,
- store_email: storeEmailAddressFromOnboardingProfile,
- },
+ is_store_country_set: isStoreCountrySet = false,
+ industry: industryFromOnboardingProfile = [],
+ business_choice: businessChoiceFromOnboardingProfile = '',
+ is_agree_marketing: isOptInMarketingFromOnboardingProfile = false,
+ store_email: storeEmailAddressFromOnboardingProfile = '',
+ } = {},
currentUserEmail,
} = context;
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/auto-block-preview.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/auto-block-preview.tsx
index 22e9b9e2127..03f7c2d42d1 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/auto-block-preview.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/auto-block-preview.tsx
@@ -40,6 +40,7 @@ import { useQuery } from '@woocommerce/navigation';
import clsx from 'clsx';
import { SelectedBlockContext } from './context/selected-block-ref-context';
import { isFullComposabilityFeatureAndAPIAvailable } from './utils/is-full-composability-enabled';
+import { useInsertPatternByName } from './hooks/use-insert-pattern-by-name';
// @ts-ignore No types for this exist yet.
const { Provider: DisabledProvider } = Disabled.Context;
@@ -170,6 +171,8 @@ function ScaledBlockPreview( {
const isResizing = useContext( IsResizingContext );
const query = useQuery();
+ const { insertPatternByName } = useInsertPatternByName();
+
useAddAutoBlockPreviewEventListenersAndObservers(
{
documentElement: iframeRef,
@@ -188,6 +191,7 @@ function ScaledBlockPreview( {
updatePopoverPosition,
setLogoBlockIds,
setContentHeight,
+ insertPatternByName,
}
);
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/block-editor-container.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/block-editor-container.tsx
index 3a21a8b2eed..9bc29e8745b 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/block-editor-container.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/block-editor-container.tsx
@@ -4,7 +4,6 @@
* External dependencies
*/
import {
- privateApis as blockEditorPrivateApis,
store as blockEditorStore,
// @ts-expect-error No types for this exist yet.
} from '@wordpress/block-editor';
@@ -32,12 +31,10 @@ import { useEditorBlocks } from './hooks/use-editor-blocks';
import { useScrollOpacity } from './hooks/use-scroll-opacity';
import {
PRODUCT_HERO_PATTERN_BUTTON_STYLE,
- findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate,
-} from './utils/hero-pattern';
+ findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate,
+} from './utils/black-background-pattern-update-button';
import { useIsActiveNewNeutralVariation } from './hooks/use-is-active-new-neutral-variation';
-const { GlobalStylesContext } = unlock( blockEditorPrivateApis );
-
export const BlockEditorContainer = () => {
const settings = useSiteEditorSettings();
@@ -97,39 +94,42 @@ export const BlockEditorContainer = () => {
// @ts-expect-error No types for this exist yet.
const { updateBlockAttributes } = useDispatch( blockEditorStore );
- // @ts-expect-error No types for this exist yet.
- const { user } = useContext( GlobalStylesContext );
-
const isActiveNewNeutralVariation = useIsActiveNewNeutralVariation();
useEffect( () => {
if ( ! isActiveNewNeutralVariation ) {
- findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate(
+ findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate(
blocks,
- ( block: BlockInstance ) => {
- updateBlockAttributes( block.clientId, {
+ ( buttonBlocks: BlockInstance[] ) => {
+ const buttonBlockClientIds = buttonBlocks.map(
+ ( { clientId } ) => clientId
+ );
+
+ updateBlockAttributes( buttonBlockClientIds, {
style: {},
} );
}
);
+
return;
}
- findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate(
+
+ findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate(
blocks,
- ( block: BlockInstance ) => {
- updateBlockAttributes( block.clientId, {
+ ( buttonBlocks: BlockInstance[] ) => {
+ const buttonBlockClientIds = buttonBlocks.map(
+ ( { clientId } ) => clientId
+ );
+ updateBlockAttributes( buttonBlockClientIds, {
style: PRODUCT_HERO_PATTERN_BUTTON_STYLE,
// This is necessary; otherwise, the style won't be applied on the frontend during the style variation change.
className: '',
} );
}
);
- }, [
- blocks,
- isActiveNewNeutralVariation,
- updateBlockAttributes,
- user.settings.color,
- ] );
+ // Blocks are not part of the dependencies because we don't want to trigger this effect when the blocks change. This would cause an infinite loop.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [ isActiveNewNeutralVariation, updateBlockAttributes ] );
// @ts-expect-error No types for this exist yet.
const { insertBlock, removeBlock } = useDispatch( blockEditorStore );
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/block-pattern-list.jsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/block-pattern-list.jsx
deleted file mode 100644
index 0721d96061e..00000000000
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/block-pattern-list.jsx
+++ /dev/null
@@ -1,146 +0,0 @@
-// Reference: https://github.com/WordPress/gutenberg/blob/94ff2ba55379d9ad7f6bed743b20b85ff26cf56d/packages/block-editor/src/components/block-patterns-list/index.js#L1
-/* eslint-disable @woocommerce/dependency-group */
-/* eslint-disable @typescript-eslint/ban-ts-comment */
-/**
- * External dependencies
- */
-import {
- VisuallyHidden,
- __unstableComposite as Composite,
- __unstableUseCompositeState as useCompositeState,
- __unstableCompositeItem as CompositeItem,
- Tooltip,
-} from '@wordpress/components';
-import { useInstanceId } from '@wordpress/compose';
-import { __ } from '@wordpress/i18n';
-import { useSelect } from '@wordpress/data';
-import { useMemo } from '@wordpress/element';
-import { store as blockEditorStore } from '@wordpress/block-editor';
-
-/**
- * Internal dependencies
- */
-import BlockPreview from './block-preview';
-import Iframe from './iframe';
-
-const WithToolTip = ( { showTooltip, title, children } ) => {
- if ( showTooltip ) {
- return (
-
- { children }
-
- );
- }
- return <>{ children }>;
-};
-
-function BlockPattern( { pattern, onClick, onHover, composite, showTooltip } ) {
- const { blocks, viewportWidth } = pattern;
- const instanceId = useInstanceId( BlockPattern );
- const descriptionId = `block-editor-block-patterns-list__item-description-${ instanceId }`;
- const originalSettings = useSelect(
- ( select ) => select( blockEditorStore ).getSettings(),
- []
- );
- const settings = useMemo(
- () => ( { ...originalSettings, __unstableIsPreviewMode: true } ),
- [ originalSettings ]
- );
- return (
-
-
-
- {
- onClick( pattern, blocks );
- onHover?.( null );
- } }
- onMouseEnter={ () => {
- onHover?.( pattern );
- } }
- onMouseLeave={ () => onHover?.( null ) }
- aria-label={ pattern.title }
- aria-describedby={
- pattern.description ? descriptionId : undefined
- }
- >
-
- { ! showTooltip && (
-
- { pattern.title }
-
- ) }
- { !! pattern.description && (
-
- { pattern.description }
-
- ) }
-
-
-
-
- );
-}
-
-function BlockPatternPlaceholder() {
- return (
-
- );
-}
-
-function BlockPatternList( {
- isDraggable,
- blockPatterns,
- shownPatterns,
- onHover,
- onClickPattern,
- orientation,
- label = __( 'Block Patterns', 'woocommerce' ),
- showTitlesAsTooltip,
-} ) {
- const composite = useCompositeState( { orientation } );
- return (
-
- { blockPatterns.map( ( pattern ) => {
- const isShown = shownPatterns.includes( pattern );
- return isShown ? (
-
- ) : (
-
- );
- } ) }
-
- );
-}
-
-export default BlockPatternList;
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/components/style.scss b/plugins/woocommerce-admin/client/customize-store/assembler-hub/components/style.scss
index 180a4270759..149d4d42bde 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/components/style.scss
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/components/style.scss
@@ -6,7 +6,7 @@
@include custom-scrollbars-on-hover(transparent, $gray-700);
// This matches the logo padding
- padding: 0 $grid-unit-15;
+ padding: 0 6px 0 12px;
// Animation
animation-duration: 0.14s;
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/auto-block-preview-event-listener.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/auto-block-preview-event-listener.ts
index af48009ec1e..c9cfa1ea2aa 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/auto-block-preview-event-listener.ts
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/auto-block-preview-event-listener.ts
@@ -8,12 +8,39 @@ import { useEffect } from '@wordpress/element';
*/
import { isFullComposabilityFeatureAndAPIAvailable } from '../utils/is-full-composability-enabled';
+export const DISABLE_CLICK_CLASS = 'disable-click';
+
+export const ENABLE_CLICK_CLASS = 'enable-click';
+
const setStyle = ( documentElement: HTMLElement ) => {
const element = documentElement.ownerDocument.documentElement;
element.classList.add( 'block-editor-block-preview__content-iframe' );
element.style.position = 'absolute';
element.style.width = '100%';
+ // Necessary for us to prevent the block editor from showing the focus outline on blocks that we've enabled interaction on.
+ const styleBlockId = 'enable-click-styles';
+ if (
+ ! documentElement.ownerDocument.head.querySelector(
+ `#${ styleBlockId }`
+ )
+ ) {
+ const styleBlock =
+ documentElement.ownerDocument.createElement( 'style' );
+ styleBlock.setAttribute( 'type', 'text/css' );
+ styleBlock.setAttribute( 'id', styleBlockId );
+ styleBlock.innerHTML = `
+ .${ ENABLE_CLICK_CLASS }[data-type="core/button"]:hover {
+ cursor: pointer;
+ }
+ .${ ENABLE_CLICK_CLASS }:focus::after,
+ .${ ENABLE_CLICK_CLASS }.is-selected::after {
+ content: none !important;
+ }
+ `;
+ documentElement.ownerDocument.head.appendChild( styleBlock );
+ }
+
// Necessary for contentResizeListener to work.
documentElement.style.boxSizing = 'border-box';
documentElement.style.position = 'absolute';
@@ -86,8 +113,6 @@ const findAndSetLogoBlock = (
return observer;
};
-export const DISABLE_CLICK_CLASS = 'disable-click';
-
const makeInert = ( element: Element ) => {
element.setAttribute( 'inert', 'true' );
};
@@ -118,7 +143,9 @@ const addInertToAssemblerPatterns = (
for ( const disableClick of documentElement.querySelectorAll(
`[data-is-parent-block='true']`
) ) {
- makeInert( disableClick );
+ if ( ! disableClick.classList.contains( ENABLE_CLICK_CLASS ) ) {
+ makeInert( disableClick );
+ }
}
for ( const element of documentElement.querySelectorAll(
@@ -159,7 +186,9 @@ const addInertToAllInnerBlocks = ( documentElement: HTMLElement ) => {
for ( const disableClick of documentElement.querySelectorAll(
`[data-is-parent-block='true'] *`
) ) {
- makeInert( disableClick );
+ if ( ! disableClick.classList.contains( ENABLE_CLICK_CLASS ) ) {
+ makeInert( disableClick );
+ }
}
} );
@@ -258,6 +287,33 @@ export const hidePopoverWhenMouseLeaveIframe = (
};
};
+const addPatternButtonClickListener = (
+ documentElement: HTMLElement,
+ insertPatternByName: ( pattern: string ) => void
+) => {
+ const DEFAULT_PATTTERN_NAME =
+ 'woocommerce-blocks/centered-content-with-image-below';
+ const handlePatternButtonClick = () => {
+ insertPatternByName( DEFAULT_PATTTERN_NAME );
+ };
+
+ const patternButton = documentElement.querySelector(
+ '.no-blocks-insert-pattern-button'
+ );
+ if ( patternButton ) {
+ patternButton.addEventListener( 'click', handlePatternButtonClick );
+ }
+
+ return () => {
+ if ( patternButton ) {
+ patternButton.removeEventListener(
+ 'click',
+ handlePatternButtonClick
+ );
+ }
+ };
+};
+
type useAutoBlockPreviewEventListenersValues = {
documentElement: HTMLElement | null;
autoScale: boolean;
@@ -290,6 +346,7 @@ type useAutoBlockPreviewEventListenersCallbacks = {
setLogoBlockIds: ( logoBlockIds: string[] ) => void;
setContentHeight: ( contentHeight: number | null ) => void;
hidePopover: () => void;
+ insertPatternByName: ( pattern: string ) => void;
};
/**
@@ -313,6 +370,7 @@ export const useAddAutoBlockPreviewEventListenersAndObservers = (
setLogoBlockIds,
setContentHeight,
hidePopover,
+ insertPatternByName,
}: useAutoBlockPreviewEventListenersCallbacks
) => {
useEffect( () => {
@@ -338,16 +396,14 @@ export const useAddAutoBlockPreviewEventListenersAndObservers = (
setStyle( documentElement );
- if ( logoBlockIds.length === 0 ) {
- const logoObserver = findAndSetLogoBlock(
- { autoScale, documentElement },
- {
- setLogoBlockIds,
- }
- );
+ const logoObserver = findAndSetLogoBlock(
+ { autoScale, documentElement },
+ {
+ setLogoBlockIds,
+ }
+ );
- observers.push( logoObserver );
- }
+ observers.push( logoObserver );
if (
isFullComposabilityFeatureAndAPIAvailable() &&
@@ -384,6 +440,14 @@ export const useAddAutoBlockPreviewEventListenersAndObservers = (
unsubscribeCallbacks.push( removeEventListenerHidePopover );
}
+ // Add event listner to the button which will insert a default pattern
+ // when there are no patterns inserted in the block preview.
+ const removePatternButtonClickListener = addPatternButtonClickListener(
+ documentElement,
+ insertPatternByName
+ );
+ unsubscribeCallbacks.push( removePatternButtonClickListener );
+
return () => {
observers.forEach( ( observer ) => observer.disconnect() );
unsubscribeCallbacks.forEach( ( callback ) => callback() );
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/block-placeholder/use-add-no-blocks-placeholder.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/block-placeholder/use-add-no-blocks-placeholder.ts
index 7c5b5a94b5f..ebd0727182a 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/block-placeholder/use-add-no-blocks-placeholder.ts
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/block-placeholder/use-add-no-blocks-placeholder.ts
@@ -8,7 +8,7 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies
*/
import NoBlocks from '../../../assets/images/no-blocks.png';
-import { DISABLE_CLICK_CLASS } from '../auto-block-preview-event-listener';
+import { ENABLE_CLICK_CLASS } from '../auto-block-preview-event-listener';
/**
* The scope of this variable is limited to the block-placeholder folder.
@@ -37,18 +37,36 @@ export const useAddNoBlocksPlaceholder = ( {
blocks.every( ( block ) => block.name === 'core/template-part' )
) {
const noBlocksBlock = createBlock(
- 'core/cover',
+ 'core/group',
{
- url: '',
- customOverlayColor: '#F6F7F7',
- minHeight: 800,
__noBlocksPlaceholder: true,
- className: DISABLE_CLICK_CLASS,
+ className: ENABLE_CLICK_CLASS,
+ style: {
+ dimensions: {
+ minHeight: '60vh',
+ },
+ color: {
+ background: '#FAFAFA',
+ },
+ spacing: {
+ padding: {
+ top: '40px',
+ bottom: '40px',
+ },
+ },
+ },
+ layout: {
+ type: 'flex',
+ orientation: 'vertical',
+ justifyContent: 'center',
+ verticalAlignment: 'center',
+ },
},
[
createBlock( 'core/image', {
url: NoBlocks,
align: 'center',
+ className: ENABLE_CLICK_CLASS,
} ),
createBlock(
'core/group',
@@ -57,14 +75,16 @@ export const useAddNoBlocksPlaceholder = ( {
type: 'constrained',
contentSize: '350px',
},
+ className: ENABLE_CLICK_CLASS,
},
[
createBlock( 'core/paragraph', {
+ className: ENABLE_CLICK_CLASS,
align: 'center',
fontFamily: 'inter',
style: {
color: {
- text: '#000000',
+ text: '#2F2F2F',
},
},
content: __(
@@ -72,6 +92,22 @@ export const useAddNoBlocksPlaceholder = ( {
'woocommerce'
),
} ),
+ createBlock( 'core/button', {
+ align: 'center',
+ fontFamily: 'inter',
+ className: `is-style-outline ${ ENABLE_CLICK_CLASS } no-blocks-insert-pattern-button`,
+ style: {
+ border: {
+ radius: '2px',
+ color: '#007cba',
+ width: '1px',
+ },
+ color: {
+ text: '#007cba',
+ },
+ },
+ text: __( 'Add patterns', 'woocommerce' ),
+ } ),
]
),
]
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-insert-pattern-by-name.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-insert-pattern-by-name.ts
new file mode 100644
index 00000000000..324c2a0d148
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-insert-pattern-by-name.ts
@@ -0,0 +1,29 @@
+/**
+ * Internal dependencies
+ */
+import { useInsertPattern } from './use-insert-pattern';
+import { usePatterns } from './use-patterns';
+
+export const useInsertPatternByName = () => {
+ const { blockPatterns, isLoading } = usePatterns();
+ const { insertPattern } = useInsertPattern();
+
+ const insertPatternByName = ( name: string ) => {
+ if ( isLoading ) {
+ return;
+ }
+
+ const pattern = blockPatterns.find( ( p ) => p.name === name );
+
+ if ( ! pattern ) {
+ return;
+ }
+
+ insertPattern( pattern );
+ };
+
+ return {
+ insertPatternByName,
+ isLoading,
+ };
+};
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-insert-pattern.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-insert-pattern.ts
new file mode 100644
index 00000000000..36674e292c3
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-insert-pattern.ts
@@ -0,0 +1,114 @@
+/* eslint-disable @woocommerce/dependency-group */
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+/**
+ * External dependencies
+ */
+import { useCallback, useMemo, useRef } from '@wordpress/element';
+import { useSelect, useDispatch, select } from '@wordpress/data';
+import { BlockInstance, cloneBlock } from '@wordpress/blocks';
+// @ts-ignore No types for this exist yet.
+import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
+// @ts-ignore No types for this exist yet.
+import { store as coreStore } from '@wordpress/core-data';
+// @ts-ignore No types for this exist yet.
+import { store as blockEditorStore } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import { useEditorBlocks } from './use-editor-blocks';
+import {
+ findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate,
+ PRODUCT_HERO_PATTERN_BUTTON_STYLE,
+} from '../utils/black-background-pattern-update-button';
+import { useIsActiveNewNeutralVariation } from './use-is-active-new-neutral-variation';
+import { trackEvent } from '../../tracking';
+
+export const useInsertPattern = () => {
+ const isActiveNewNeutralVariation = useIsActiveNewNeutralVariation();
+
+ const currentTemplate = useSelect(
+ ( sel ) =>
+ // @ts-expect-error No types for this exist yet.
+ sel( coreStore ).__experimentalGetTemplateForLink( '/' ),
+ []
+ );
+
+ const [ blocks ] = useEditorBlocks(
+ 'wp_template',
+ currentTemplate?.id ?? ''
+ );
+
+ const blockToScroll = useRef< string | null >( null );
+
+ // @ts-expect-error No types for this exist yet.
+ const { insertBlocks } = useDispatch( blockEditorStore );
+
+ const insertableIndex = useMemo( () => {
+ return blocks.findLastIndex(
+ ( block ) => block.name === 'core/template-part'
+ );
+ }, [ blocks ] );
+
+ const insertPattern = useCallback(
+ ( pattern ) => {
+ const parsedPattern = unlock(
+ select( blockEditorStore )
+ ).__experimentalGetParsedPattern( pattern.name );
+
+ const cloneBlocks = parsedPattern.blocks.map(
+ ( blockInstance: BlockInstance ) => cloneBlock( blockInstance )
+ );
+
+ if ( ! isActiveNewNeutralVariation ) {
+ const updatedBlocks =
+ findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate(
+ cloneBlocks,
+ ( patternBlocks: BlockInstance[] ) => {
+ patternBlocks.forEach(
+ ( block: BlockInstance ) =>
+ ( block.attributes.style = {} )
+ );
+ }
+ );
+
+ insertBlocks(
+ updatedBlocks,
+ insertableIndex,
+ undefined,
+ false
+ );
+ blockToScroll.current = updatedBlocks[ 0 ].clientId;
+ } else {
+ const updatedBlocks =
+ findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate(
+ cloneBlocks,
+ ( patternBlocks: BlockInstance[] ) => {
+ patternBlocks.forEach(
+ ( block ) =>
+ ( block.attributes.style =
+ PRODUCT_HERO_PATTERN_BUTTON_STYLE )
+ );
+ }
+ );
+ insertBlocks(
+ updatedBlocks,
+ insertableIndex,
+ undefined,
+ false
+ );
+ blockToScroll.current = updatedBlocks[ 0 ].clientId;
+ }
+
+ trackEvent(
+ 'customize_your_store_assembler_pattern_sidebar_click',
+ { pattern: pattern.name }
+ );
+ },
+ [ insertBlocks, insertableIndex, isActiveNewNeutralVariation ]
+ );
+
+ return {
+ insertPattern,
+ };
+};
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-patterns.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-patterns.ts
index 639f8ad20ec..666cc15e297 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-patterns.ts
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-patterns.ts
@@ -3,7 +3,7 @@
/**
* External dependencies
*/
-import { useSelect } from '@wordpress/data';
+import { dispatch, useSelect } from '@wordpress/data';
// @ts-ignore No types for this exist yet.
import { store as coreStore } from '@wordpress/core-data';
import { useEffect, useMemo, useState } from '@wordpress/element';
@@ -17,24 +17,29 @@ import { Pattern } from '~/customize-store/types/pattern';
import { THEME_SLUG } from '~/customize-store/data/constants';
export const usePatterns = () => {
- const { blockPatterns, isLoading } = useSelect(
+ const { blockPatterns, isLoading, invalidateCache } = useSelect(
( select ) => ( {
blockPatterns: select(
coreStore
- // @ts-ignore - This is valid.
+ // @ts-expect-error -- No types for this exist yet.
).getBlockPatterns() as Pattern[],
isLoading:
- // @ts-ignore - This is valid.
+ // @ts-expect-error -- No types for this exist yet.
! select( coreStore ).hasFinishedResolution(
'getBlockPatterns'
),
- } ),
- []
+ invalidateCache: () =>
+ // @ts-expect-error -- No types for this exist yet.
+ dispatch( coreStore ).invalidateResolutionForStoreSelector(
+ 'getBlockPatterns'
+ ),
+ } )
);
return {
blockPatterns,
isLoading,
+ invalidateCache,
};
};
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-popover-handler.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-popover-handler.ts
index ac9bf2fea9f..d0370121ef5 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-popover-handler.ts
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/hooks/use-popover-handler.ts
@@ -3,6 +3,11 @@
*/
import { useState } from 'react';
+/**
+ * Internal dependencies
+ */
+import { ENABLE_CLICK_CLASS } from './auto-block-preview-event-listener';
+
export enum PopoverStatus {
VISIBLE = 'VISIBLE',
HIDDEN = 'HIDDEN',
@@ -37,6 +42,12 @@ export const usePopoverHandler = () => {
defaultVirtualElement
);
+ const hidePopover = () => {
+ setPopoverStatus( PopoverStatus.HIDDEN );
+ clickedClientId = null;
+ hoveredClientId = null;
+ };
+
const updatePopoverPosition = ( {
event,
clickedBlockClientId,
@@ -50,6 +61,15 @@ export const usePopoverHandler = () => {
'.woocommerce-customize-store-assembler > iframe[name="editor-canvas"]'
) as HTMLElement;
+ const target = event.target as HTMLElement;
+
+ // If the hover event is over elements with an ENABLE_CLICK_CLASS, hide the popover.
+ // This is because it's likely the "No Blocks" placeholder and we don't want the popover to show since its interactive.
+ if ( target.classList.contains( ENABLE_CLICK_CLASS ) ) {
+ hidePopover();
+ return;
+ }
+
clickedClientId =
clickedBlockClientId === null
? clickedClientId
@@ -81,12 +101,6 @@ export const usePopoverHandler = () => {
clickedClientId = null;
};
- const hidePopover = () => {
- setPopoverStatus( PopoverStatus.HIDDEN );
- clickedClientId = null;
- hoveredClientId = null;
- };
-
return [
popoverStatus,
virtualElement,
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx
index c19a5a61c54..375095a4ece 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx
@@ -51,6 +51,8 @@ import { onBackButtonClicked } from '../utils';
import { getNewPath } from '@woocommerce/navigation';
import useBodyClass from '../hooks/use-body-class';
+import './tracking';
+
const { RouterProvider } = unlock( routerPrivateApis );
addFilter(
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/index.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/index.tsx
index f093d87117d..26c0a85c4a5 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/index.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/index.tsx
@@ -81,34 +81,36 @@ export const FontPairing = () => {
: FONT_PAIRINGS_WHEN_AI_IS_OFFLINE;
}
- if ( ! trackingAllowed || ! isFontLibraryAvailable ) {
- return FONT_PAIRINGS_WHEN_USER_DID_NOT_ALLOW_TRACKING.map(
- ( pair ) => {
- const fontFamilies = pair.settings.typography.fontFamilies;
+ const defaultFonts = FONT_PAIRINGS_WHEN_USER_DID_NOT_ALLOW_TRACKING.map(
+ ( pair ) => {
+ const fontFamilies = pair.settings.typography.fontFamilies;
- const fonts = baseFontFamilies.theme.filter(
- ( baseFontFamily ) =>
- fontFamilies.theme.some(
- ( themeFont ) =>
- themeFont.fontFamily === baseFontFamily.name
- )
- );
+ const fonts = baseFontFamilies.theme.filter(
+ ( baseFontFamily ) =>
+ fontFamilies.theme.some(
+ ( themeFont ) =>
+ themeFont.fontFamily === baseFontFamily.name
+ )
+ );
- return {
- ...pair,
- settings: {
- typography: {
- fontFamilies: {
- theme: fonts,
- },
+ return {
+ ...pair,
+ settings: {
+ typography: {
+ fontFamilies: {
+ theme: fonts,
},
},
- };
- }
- );
+ },
+ };
+ }
+ );
+
+ if ( ! trackingAllowed || ! isFontLibraryAvailable ) {
+ return defaultFonts;
}
- return FONT_PAIRINGS_WHEN_AI_IS_OFFLINE.map( ( pair ) => {
+ const customFonts = FONT_PAIRINGS_WHEN_AI_IS_OFFLINE.map( ( pair ) => {
const fontFamilies = pair.settings.typography.fontFamilies;
const fonts = custom.filter( ( customFont ) =>
fontFamilies.theme.some(
@@ -126,7 +128,9 @@ export const FontPairing = () => {
},
},
};
- }, [] );
+ } );
+
+ return [ ...defaultFonts, ...customFonts ];
}, [
aiOnline,
aiSuggestions?.lookAndFeel,
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/index.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/index.tsx
index abad2468f2a..49881619701 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/index.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/index.tsx
@@ -10,11 +10,11 @@ import { memo, useCallback, useContext } from '@wordpress/element';
* Internal dependencies
*/
import { SidebarNavigationScreenColorPalette } from './sidebar-navigation-screen-color-palette';
-import { SidebarNavigationScreenFooter } from './sidebar-navigation-screen-footer';
-import { SidebarNavigationScreenHeader } from './sidebar-navigation-screen-header';
-import { SidebarNavigationScreenHomepage } from './sidebar-navigation-screen-homepage-ptk/sidebar-navigation-screen-homepage';
+import { SidebarNavigationScreenFooter } from './sidebar-navigation-screen-footer/sidebar-navigation-screen-footer';
+import { SidebarNavigationScreenHeader } from './sidebar-navigation-screen-header/sidebar-navigation-screen-header';
+import { SidebarNavigationScreenHomepage } from './sidebar-navigation-screen-homepage/sidebar-navigation-screen-homepage';
import { SidebarNavigationScreenMain } from './sidebar-navigation-screen-main';
-import { SidebarNavigationScreenTypography } from './sidebar-navigation-screen-typography';
+import { SidebarNavigationScreenTypography } from './sidebar-navigation-screen-typography/sidebar-navigation-screen-typography';
// import { SidebarNavigationScreenPages } from './sidebar-navigation-screen-pages';
import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
@@ -27,8 +27,8 @@ import {
SidebarNavigationContext,
} from '../components/sidebar';
import { SidebarNavigationScreenLogo } from './sidebar-navigation-screen-logo';
-import { isPatternToolkitFullComposabilityFeatureFlagEnabled } from '../utils/is-full-composability-enabled';
-import { SidebarNavigationScreenHomepagePTK } from './sidebar-navigation-screen-homepage-ptk';
+import { isFullComposabilityFeatureAndAPIAvailable } from '../utils/is-full-composability-enabled';
+import { SidebarNavigationScreenHomepagePTK } from './sidebar-navigation-screen-homepage-ptk/sidebar-navigation-screen-homepage-ptk';
const getComponentByPathParams = (
params: string,
@@ -71,7 +71,7 @@ const getComponentByPathParams = (
}
if (
- isPatternToolkitFullComposabilityFeatureFlagEnabled() &&
+ isFullComposabilityFeatureAndAPIAvailable() &&
params?.includes( '/customize-store/assembler-hub/homepage' )
) {
return (
@@ -82,7 +82,7 @@ const getComponentByPathParams = (
}
if (
- ! isPatternToolkitFullComposabilityFeatureFlagEnabled() &&
+ ! isFullComposabilityFeatureAndAPIAvailable() &&
params === '/customize-store/assembler-hub/homepage'
) {
return (
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/navigation-extra-screen/style.scss b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/navigation-extra-screen/style.scss
index f8ac8e2193c..f5fa9919e21 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/navigation-extra-screen/style.scss
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/navigation-extra-screen/style.scss
@@ -1,4 +1,5 @@
.woocommerce-customize-store-edit-site-layout__sidebar-extra {
width: 380px;
- border-right: 1px solid rgba(0, 0, 0, 0.05);
+ padding: 16px 6px 16px 6px;
+ background-color: #f5f5f5;
}
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/sidebar-pattern-screen.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/sidebar-pattern-screen.tsx
index 596e00e7eeb..be21f55418a 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/sidebar-pattern-screen.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/sidebar-pattern-screen.tsx
@@ -1,25 +1,15 @@
/**
* External dependencies
*/
-import {
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from '@wordpress/element';
+import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
import { useAsyncList } from '@wordpress/compose';
-import { useSelect, useDispatch, select } from '@wordpress/data';
-import { BlockInstance, cloneBlock } from '@wordpress/blocks';
+import { useSelect } from '@wordpress/data';
+import { BlockInstance } from '@wordpress/blocks';
import { close } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { getNewPath, navigateTo } from '@woocommerce/navigation';
import { capitalize } from 'lodash';
import { Button, Spinner } from '@wordpress/components';
-import {
- unlock,
- // @ts-expect-error No types for this exist yet.
-} from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-expect-error No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { useIsSiteEditorLoading } from '@wordpress/edit-site/build-module/components/layout/hooks';
@@ -31,7 +21,6 @@ import {
// eslint-disable-next-line @woocommerce/dependency-group
import {
__experimentalBlockPatternsList as BlockPatternList,
- store as blockEditorStore,
// @ts-expect-error No types for this exist yet.
} from '@wordpress/block-editor';
@@ -43,86 +32,17 @@ import './style.scss';
import { useEditorBlocks } from '../../hooks/use-editor-blocks';
import { PATTERN_CATEGORIES } from './categories';
import { THEME_SLUG } from '~/customize-store/data/constants';
-import { Pattern } from '~/customize-store/types/pattern';
import {
- findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate,
+ findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate,
PRODUCT_HERO_PATTERN_BUTTON_STYLE,
-} from '../../utils/hero-pattern';
+} from '../../utils/black-background-pattern-update-button';
import { useIsActiveNewNeutralVariation } from '../../hooks/use-is-active-new-neutral-variation';
-
-/**
- * Adds a 'is-added' CSS class to each pattern preview element in the pattern list that matches a block's pattern name.
- * This function iterates through an array of blocks added in the page, extracts the pattern name from each block's metadata,
- * and finds the corresponding pattern preview element in the pattern list by its ID. If found, the 'is-added' class is added to the element.
- */
-const addIsAddedClassToPatternPreview = (
- patternListEl: HTMLElement,
- blocks: BlockInstance[]
-) => {
- patternListEl.querySelectorAll( '.is-added' ).forEach( ( element ) => {
- element.classList.remove( 'is-added' );
- } );
-
- blocks.forEach( ( block ) => {
- const patterName = block.attributes.metadata?.patternName;
- if ( ! patterName ) {
- return;
- }
-
- const element = patternListEl.querySelector( `[id="${ patterName }"]` );
-
- if ( element ) {
- element.classList.add( 'is-added' );
- }
- } );
-};
-
-/**
- * Sorts patterns by category. For 'intro' and 'about' categories
- * prioritized DotCom Patterns. For intro category, it also prioritizes the "centered-content-with-image-below" pattern.
- * For other categories, it simply sorts patterns to prioritize Woo Patterns.
- */
-const sortPatternsByCategory = (
- patterns: Pattern[],
- category: keyof typeof PATTERN_CATEGORIES
-) => {
- const prefix = 'woocommerce-blocks';
- if ( category === 'intro' || category === 'about' ) {
- return patterns.sort( ( a, b ) => {
- if (
- a.name ===
- 'woocommerce-blocks/centered-content-with-image-below'
- ) {
- return -1;
- }
-
- if (
- b.name ===
- 'woocommerce-blocks/centered-content-with-image-below'
- ) {
- return 1;
- }
-
- if ( a.name.includes( prefix ) && ! b.name.includes( prefix ) ) {
- return 1;
- }
- if ( ! a.name.includes( prefix ) && b.name.includes( prefix ) ) {
- return -1;
- }
- return 0;
- } );
- }
-
- return patterns.sort( ( a, b ) => {
- if ( a.name.includes( prefix ) && ! b.name.includes( prefix ) ) {
- return -1;
- }
- if ( ! a.name.includes( prefix ) && b.name.includes( prefix ) ) {
- return 1;
- }
- return 0;
- } );
-};
+import {
+ sortPatternsByCategory,
+ addIsAddedClassToPatternPreview,
+} from './utils';
+import { trackEvent } from '~/customize-store/tracking';
+import { useInsertPattern } from '../../hooks/use-insert-pattern';
export const SidebarPatternScreen = ( { category }: { category: string } ) => {
const { patterns, isLoading } = usePatternsByCategory( category );
@@ -139,28 +59,37 @@ export const SidebarPatternScreen = ( { category }: { category: string } ) => {
const patternWithPatchedProductHeroPattern =
patternsWithoutThemePatterns.map( ( pattern ) => {
if (
- pattern.name !== 'woocommerce-blocks/just-arrived-full-hero'
+ pattern.name !==
+ 'woocommerce-blocks/just-arrived-full-hero' &&
+ pattern.name !==
+ 'woocommerce-blocks/featured-category-cover-image'
) {
return pattern;
}
if ( ! isActiveNewNeutralVariation ) {
const blocks =
- findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate(
+ findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate(
pattern.blocks,
- ( block: BlockInstance ) => {
- block.attributes.style = {};
+ ( patternBlocks: BlockInstance[] ) => {
+ patternBlocks.forEach(
+ ( block: BlockInstance ) =>
+ ( block.attributes.style = {} )
+ );
}
);
return { ...pattern, blocks };
}
const blocks =
- findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate(
+ findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate(
pattern.blocks,
- ( block: BlockInstance ) => {
- block.attributes.style =
- PRODUCT_HERO_PATTERN_BUTTON_STYLE;
+ ( patternBlocks: BlockInstance[] ) => {
+ patternBlocks.forEach(
+ ( block ) =>
+ ( block.attributes.style =
+ PRODUCT_HERO_PATTERN_BUTTON_STYLE )
+ );
}
);
@@ -261,31 +190,7 @@ export const SidebarPatternScreen = ( { category }: { category: string } ) => {
};
}, [ isEditorLoading ] );
- // @ts-expect-error No types for this exist yet.
- const { insertBlocks } = useDispatch( blockEditorStore );
-
- const insertableIndex = useMemo( () => {
- return blocks.findLastIndex(
- ( block ) => block.name === 'core/template-part'
- );
- }, [ blocks ] );
-
- const onClickPattern = useCallback(
- ( pattern ) => {
- const parsedPattern = unlock(
- select( blockEditorStore )
- ).__experimentalGetParsedPattern( pattern.name );
-
- const cloneBlocks = parsedPattern.blocks.map(
- ( blockInstance: BlockInstance ) => cloneBlock( blockInstance )
- );
-
- insertBlocks( cloneBlocks, insertableIndex, undefined, false );
-
- blockToScroll.current = cloneBlocks[ 0 ].clientId;
- },
- [ insertBlocks, insertableIndex ]
- );
+ const { insertPattern } = useInsertPattern();
return (
{
`/customize-store/assembler-hub/homepage`,
{}
);
-
navigateTo( { url: homepageUrl } );
+ trackEvent(
+ 'customize_your_store_assembler_pattern_sidebar_close'
+ );
} }
iconSize={ 18 }
icon={ close }
@@ -348,7 +255,7 @@ export const SidebarPatternScreen = ( { category }: { category: string } ) => {
0,
patternPagination
) }
- onClickPattern={ onClickPattern }
+ onClickPattern={ insertPattern }
label={ 'Homepage' }
orientation="vertical"
category={ category }
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/style.scss b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/style.scss
index 00e25fe65f2..eb35a88ebfd 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/style.scss
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/style.scss
@@ -3,10 +3,16 @@
display: flex;
flex-direction: column;
gap: 10px;
- background-color: #f0f0f0;
- padding: 25px;
+ background-color: #fff;
+ border-radius: 12px;
+ box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.05);
+ padding: 16px 24px 24px 24px;
overflow: auto;
+ .block-editor-block-preview__container {
+ margin-bottom: 2px;
+ }
+
.woocommerce-customize-store-edit-site-layout__sidebar-extra__pattern__header {
display: flex;
align-items: center;
@@ -15,6 +21,7 @@
h1 {
font-size: 16px;
+ padding: 0;
}
span {
@@ -43,10 +50,9 @@
&.is-added {
.auto-block-preview__container,
.block-editor-block-preview__container {
- // WIP: This is a temporary CSS. Replace red with the actual color.
- box-shadow: 0 0 0 1.5px #fff,
- 0 0 0 3.5px var(--gutenberg-gray-900, #1e1e1e);
- border-color: var(--gutenberg-gray-900, #1e1e1e);
+ box-shadow: 0 0 0 0 #fff,
+ 0 0 0 2px var(--wp-admin-theme-color, #3858e9);
+ border-color: var(--wp-admin-theme-color);
border-radius: 4px;
&::after {
outline: none;
@@ -58,8 +64,8 @@
&:focus {
.auto-block-preview__container,
.block-editor-block-preview__container {
- box-shadow: 0 0 0 1.5px #fff,
- 0 0 0 3.5px var(--wp-admin-theme-color, #3858e9);
+ box-shadow: 0 0 0 0 #fff,
+ 0 0 0 2px var(--wp-admin-theme-color, #3858e9);
border-color: var(--wp-admin-theme-color);
border-radius: 4px;
&::after {
@@ -68,4 +74,8 @@
}
}
}
+
+ div.block-editor-block-patterns-list > div:nth-last-child(2) {
+ margin-bottom: 0;
+ }
}
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/utils.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/utils.ts
new file mode 100644
index 00000000000..40c77d46933
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/pattern-screen/utils.ts
@@ -0,0 +1,167 @@
+/**
+ * External dependencies
+ */
+import { BlockInstance } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { Pattern } from '~/customize-store/types/pattern';
+import { PATTERN_CATEGORIES } from './categories';
+
+/**
+ * Adds a 'is-added' CSS class to each pattern preview element in the pattern list that matches a block's pattern name.
+ * This function iterates through an array of blocks added in the page, extracts the pattern name from each block's metadata,
+ * and finds the corresponding pattern preview element in the pattern list by its ID. If found, the 'is-added' class is added to the element.
+ */
+export const addIsAddedClassToPatternPreview = (
+ patternListEl: HTMLElement,
+ blocks: BlockInstance[]
+) => {
+ patternListEl.querySelectorAll( '.is-added' ).forEach( ( element ) => {
+ element.classList.remove( 'is-added' );
+ } );
+
+ blocks.forEach( ( block ) => {
+ const patterName = block.attributes.metadata?.patternName;
+ if ( ! patterName ) {
+ return;
+ }
+
+ const element = patternListEl.querySelector( `[id="${ patterName }"]` );
+
+ if ( element ) {
+ element.classList.add( 'is-added' );
+ }
+ } );
+};
+
+const orderPatternList = {
+ intro: [
+ 'Intro: Two column with content and image',
+ 'Heading with image and two columns below',
+ 'Fullwidth content with background image',
+ 'Two column with image and content',
+ 'Centered heading with two column text',
+ 'Content with button and fullwidth image',
+ 'Center-aligned content overlaid on an image',
+ 'Left-aligned content overlaid on an image',
+ 'Centered Content',
+ 'Large left-aligned heading',
+ 'Fullwidth image with content and button',
+ 'Pull right with wide image below',
+ ],
+ about: [
+ 'Content right with image left',
+ 'Content left with image right',
+ 'Heading left and content right',
+ 'Four image grid, content on the left',
+ 'Content with grid of images on right',
+ 'Heading with two media columns',
+ 'Heading with content and large image below',
+ 'Centered heading and button',
+ 'Content left, image right',
+ 'Tall content with image left',
+ 'Fullwidth image, content pull right',
+ 'Right-aligned Content',
+ 'Large heading with content on right',
+ 'Tall content with image right',
+ 'Spread right, heavy text',
+ 'Heading with button and text',
+ 'Left-aligned content',
+ 'Pull left, fullwidth image',
+ ],
+ services: [
+ 'Three columns with images and content',
+ 'Heading with four text sections',
+ 'Two columns with images',
+ 'Heading with six text sections',
+ 'Headings left, content right',
+ ],
+} as Record< string, string[] >;
+
+const orderByPriority = (
+ aElementTitle: string,
+ bElementTitle: string,
+ category: string
+) => {
+ const aIndex = orderPatternList[ category ]?.indexOf( aElementTitle );
+ const bIndex = orderPatternList[ category ]?.indexOf( bElementTitle );
+
+ if ( aIndex === -1 && bIndex === -1 ) {
+ return null;
+ }
+
+ if ( aIndex > -1 && bIndex > -1 ) {
+ return aIndex - bIndex;
+ }
+
+ if ( aIndex === -1 && bIndex > -1 ) {
+ return 1;
+ }
+ if ( bIndex === -1 && aIndex > -1 ) {
+ return -1;
+ }
+};
+
+/**
+ * Sorts patterns by category and priority based on the orderPatternList object . For 'intro' and 'about' categories
+ * prioritized DotCom Patterns. For intro category, it also prioritizes the "centered-content-with-image-below" pattern.
+ * For other categories, it simply sorts patterns to prioritize Woo Patterns.
+ */
+export const sortPatternsByCategory = (
+ patterns: Pattern[],
+ category: keyof typeof PATTERN_CATEGORIES
+) => {
+ const prefix = 'woocommerce-blocks';
+ if ( category === 'intro' || category === 'about' ) {
+ return patterns.sort( ( a, b ) => {
+ if (
+ a.name ===
+ 'woocommerce-blocks/centered-content-with-image-below'
+ ) {
+ return -1;
+ }
+
+ if (
+ b.name ===
+ 'woocommerce-blocks/centered-content-with-image-below'
+ ) {
+ return 1;
+ }
+
+ const priority = orderByPriority( a.title, b.title, category );
+
+ if ( typeof priority === 'number' ) {
+ return priority;
+ }
+
+ if ( a.name.includes( prefix ) && ! b.name.includes( prefix ) ) {
+ return 1;
+ }
+
+ if ( ! a.name.includes( prefix ) && b.name.includes( prefix ) ) {
+ return -1;
+ }
+
+ // If neither title is in the list, keep their original order
+ return 0;
+ } );
+ }
+
+ return patterns.sort( ( a, b ) => {
+ const priority = orderByPriority( a.title, b.title, category );
+
+ if ( typeof priority === 'number' ) {
+ return priority;
+ }
+
+ if ( a.name.includes( prefix ) && ! b.name.includes( prefix ) ) {
+ return -1;
+ }
+ if ( ! a.name.includes( prefix ) && b.name.includes( prefix ) ) {
+ return 1;
+ }
+ return 0;
+ } );
+};
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer/sidebar-navigation-screen-footer.tsx
similarity index 89%
rename from plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer.tsx
rename to plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer/sidebar-navigation-screen-footer.tsx
index c5ef4e250cc..f55d260306a 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer/sidebar-navigation-screen-footer.tsx
@@ -19,21 +19,26 @@ import { store as coreStore } from '@wordpress/core-data';
/**
* Internal dependencies
*/
-import { SidebarNavigationScreen } from './sidebar-navigation-screen';
+import { SidebarNavigationScreen } from '../sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
-import { useEditorBlocks } from '../hooks/use-editor-blocks';
-import { usePatternsByCategory } from '../hooks/use-patterns';
-import { HighlightedBlockContext } from '../context/highlighted-block-context';
-import { useEditorScroll } from '../hooks/use-editor-scroll';
-import { useSelectedPattern } from '../hooks/use-selected-pattern';
-import { findPatternByBlock } from './utils';
-import BlockPatternList from '../block-pattern-list';
+import { useEditorBlocks } from '../../hooks/use-editor-blocks';
+import { usePatternsByCategory } from '../../hooks/use-patterns';
+import { HighlightedBlockContext } from '../../context/highlighted-block-context';
+import { useEditorScroll } from '../../hooks/use-editor-scroll';
+import { useSelectedPattern } from '../../hooks/use-selected-pattern';
+import { findPatternByBlock } from '../utils';
+import {
+ __experimentalBlockPatternsList as BlockPatternList,
+ // @ts-expect-error No types for this exist yet.
+} from '@wordpress/block-editor';
import { CustomizeStoreContext } from '~/customize-store/assembler-hub';
import { FlowType } from '~/customize-store/types';
import { footerTemplateId } from '~/customize-store/data/homepageTemplates';
import { useSelect } from '@wordpress/data';
import { trackEvent } from '~/customize-store/tracking';
+import './style.scss';
+
const SUPPORTED_FOOTER_PATTERNS = [
'woocommerce-blocks/footer-with-3-menus',
'woocommerce-blocks/footer-simple-menu',
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer/style.scss b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer/style.scss
new file mode 100644
index 00000000000..edcfa8880f6
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer/style.scss
@@ -0,0 +1,38 @@
+.woocommerce-customize-store__sidebar-footer-content {
+ .block-editor-block-preview__container {
+ margin-bottom: 2px;
+ }
+
+ .block-editor-block-patterns-list__item {
+ .auto-block-preview__container,
+ .block-editor-block-preview__container {
+ border: none;
+ }
+
+ &.is-selected {
+ .auto-block-preview__container,
+ .block-editor-block-preview__container {
+ box-shadow: 0 0 0 0 #fff,
+ 0 0 0 2px var(--wp-admin-theme-color, #3858e9);
+ border-color: var(--wp-admin-theme-color);
+ border-radius: 4px;
+ &::after {
+ outline: none;
+ }
+ }
+ }
+
+ &:hover {
+ .auto-block-preview__container,
+ .block-editor-block-preview__container {
+ box-shadow: 0 0 0 0 #fff,
+ 0 0 0 2px var(--wp-admin-theme-color, #3858e9);
+ border-color: var(--wp-admin-theme-color);
+ border-radius: 4px;
+ &::after {
+ outline: none;
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header/sidebar-navigation-screen-header.tsx
similarity index 88%
rename from plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header.tsx
rename to plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header/sidebar-navigation-screen-header.tsx
index 667bc335da2..413d374aeeb 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header/sidebar-navigation-screen-header.tsx
@@ -20,20 +20,25 @@ import { store as coreStore } from '@wordpress/core-data';
/**
* Internal dependencies
*/
-import { SidebarNavigationScreen } from './sidebar-navigation-screen';
+import { SidebarNavigationScreen } from '../sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
-import { usePatternsByCategory } from '../hooks/use-patterns';
-import { useSelectedPattern } from '../hooks/use-selected-pattern';
-import { useEditorBlocks } from '../hooks/use-editor-blocks';
-import { HighlightedBlockContext } from '../context/highlighted-block-context';
-import { useEditorScroll } from '../hooks/use-editor-scroll';
-import { findPatternByBlock } from './utils';
-import BlockPatternList from '../block-pattern-list';
+import { usePatternsByCategory } from '../../hooks/use-patterns';
+import { useSelectedPattern } from '../../hooks/use-selected-pattern';
+import { useEditorBlocks } from '../../hooks/use-editor-blocks';
+import { HighlightedBlockContext } from '../../context/highlighted-block-context';
+import { useEditorScroll } from '../../hooks/use-editor-scroll';
+import { findPatternByBlock } from '../utils';
+import {
+ __experimentalBlockPatternsList as BlockPatternList,
+ // @ts-expect-error No types for this exist yet.
+} from '@wordpress/block-editor';
import { CustomizeStoreContext } from '~/customize-store/assembler-hub';
import { FlowType } from '~/customize-store/types';
import { headerTemplateId } from '~/customize-store/data/homepageTemplates';
import { trackEvent } from '~/customize-store/tracking';
+import './style.scss';
+
const SUPPORTED_HEADER_PATTERNS = [
'woocommerce-blocks/header-centered-menu',
'woocommerce-blocks/header-essential',
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header/style.scss b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header/style.scss
new file mode 100644
index 00000000000..6a82a4e6337
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header/style.scss
@@ -0,0 +1,38 @@
+.woocommerce-customize-store__sidebar-header-content {
+ .block-editor-block-preview__container {
+ margin-bottom: 2px;
+ }
+
+ .block-editor-block-patterns-list__item {
+ .auto-block-preview__container,
+ .block-editor-block-preview__container {
+ border: none;
+ }
+
+ &.is-selected {
+ .auto-block-preview__container,
+ .block-editor-block-preview__container {
+ box-shadow: 0 0 0 0 #fff,
+ 0 0 0 2px var(--wp-admin-theme-color, #3858e9);
+ border-color: var(--wp-admin-theme-color);
+ border-radius: 4px;
+ &::after {
+ outline: none;
+ }
+ }
+ }
+
+ &:hover {
+ .auto-block-preview__container,
+ .block-editor-block-preview__container {
+ box-shadow: 0 0 0 0 #fff,
+ 0 0 0 2px var(--wp-admin-theme-color, #3858e9);
+ border-color: var(--wp-admin-theme-color);
+ border-radius: 4px;
+ &::after {
+ outline: none;
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage-ptk.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage-ptk/sidebar-navigation-screen-homepage-ptk.tsx
similarity index 67%
rename from plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage-ptk.tsx
rename to plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage-ptk/sidebar-navigation-screen-homepage-ptk.tsx
index 186a5b519d3..4ba091e6355 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage-ptk.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage-ptk/sidebar-navigation-screen-homepage-ptk.tsx
@@ -11,6 +11,7 @@ import {
Modal,
// @ts-ignore No types for this exist yet.
__experimentalNavigatorButton as NavigatorButton,
+ Spinner,
// @ts-ignore No types for this exist yet.
} from '@wordpress/components';
import {
@@ -32,20 +33,23 @@ import SidebarNavigationItem from '@wordpress/edit-site/build-module/components/
* Internal dependencies
*/
import { ADMIN_URL } from '~/utils/admin-settings';
-import { SidebarNavigationScreen } from './sidebar-navigation-screen';
-
+import { SidebarNavigationScreen } from '../sidebar-navigation-screen';
import { trackEvent } from '~/customize-store/tracking';
-import { CustomizeStoreContext } from '..';
+import { CustomizeStoreContext } from '../..';
import { Link } from '@woocommerce/components';
-import { PATTERN_CATEGORIES } from './pattern-screen/categories';
+import { PATTERN_CATEGORIES } from '../pattern-screen/categories';
import { capitalize } from 'lodash';
import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
import { useSelect } from '@wordpress/data';
import { useNetworkStatus } from '~/utils/react-hooks/use-network-status';
-import { isIframe, sendMessageToParent } from '~/customize-store/utils';
-import { useEditorBlocks } from '../hooks/use-editor-blocks';
-import { isTrackingAllowed } from '../utils/is-tracking-allowed';
+import { useEditorBlocks } from '../../hooks/use-editor-blocks';
+import { isTrackingAllowed } from '../../utils/is-tracking-allowed';
import clsx from 'clsx';
+import './style.scss';
+import { usePatterns } from '~/customize-store/assembler-hub/hooks/use-patterns';
+import { THEME_SLUG } from '~/customize-store/data/constants';
+import apiFetch from '@wordpress/api-fetch';
+import { enableTracking } from '~/customize-store/design-without-ai/services';
const isActiveElement = ( path: string | undefined, category: string ) => {
if ( path?.includes( category ) ) {
@@ -58,7 +62,7 @@ export const SidebarNavigationScreenHomepagePTK = ( {
}: {
onNavigateBackClick: () => void;
} ) => {
- const { context, sendEvent } = useContext( CustomizeStoreContext );
+ const { context } = useContext( CustomizeStoreContext );
const isNetworkOffline = useNetworkStatus();
const isPTKPatternsAPIAvailable = context.isPTKPatternsAPIAvailable;
@@ -105,23 +109,52 @@ export const SidebarNavigationScreenHomepagePTK = ( {
}, initialAccumulator );
}, [ blocks ] );
- let notice;
- if ( isNetworkOffline ) {
- notice = __(
- "Looks like we can't detect your network. Please double-check your internet connection and refresh the page.",
- 'woocommerce'
- );
- } else if ( ! isPTKPatternsAPIAvailable ) {
- notice = __(
- "Unfortunately, we're experiencing some technical issues — please come back later to access more patterns.",
- 'woocommerce'
- );
- } else if ( ! isTrackingAllowed() ) {
- notice = __(
- 'Opt in to usage tracking to get access to more patterns.',
- 'woocommerce'
- );
- }
+ const {
+ blockPatterns,
+ isLoading: isLoadingPatterns,
+ invalidateCache,
+ } = usePatterns();
+
+ const patternsFromPTK = blockPatterns.filter(
+ ( pattern ) =>
+ ! pattern.name.includes( THEME_SLUG ) &&
+ ! pattern.name.includes( 'woocommerce' ) &&
+ pattern.source !== 'core' &&
+ pattern.source !== 'pattern-directory/featured' &&
+ pattern.source !== 'pattern-directory/theme' &&
+ pattern.source !== 'pattern-directory/core'
+ );
+
+ const notice = useMemo( () => {
+ let noticeText;
+ if ( isNetworkOffline ) {
+ noticeText = __(
+ "Looks like we can't detect your network. Please double-check your internet connection and refresh the page.",
+ 'woocommerce'
+ );
+ } else if ( ! isPTKPatternsAPIAvailable ) {
+ noticeText = __(
+ "Unfortunately, we're experiencing some technical issues — please come back later to access more patterns.",
+ 'woocommerce'
+ );
+ } else if ( ! isTrackingAllowed() ) {
+ noticeText = __(
+ 'Opt in to usage tracking to get access to more patterns.',
+ 'woocommerce'
+ );
+ } else if ( ! isLoadingPatterns && patternsFromPTK.length === 0 ) {
+ noticeText = __(
+ 'Unfortunately, a technical issue is preventing more patterns from being displayed. Please try again later.',
+ 'woocommerce'
+ );
+ }
+ return noticeText;
+ }, [
+ isNetworkOffline,
+ isPTKPatternsAPIAvailable,
+ isLoadingPatterns,
+ patternsFromPTK.length,
+ ] );
const [ isModalOpen, setIsModalOpen ] = useState( false );
@@ -131,6 +164,8 @@ export const SidebarNavigationScreenHomepagePTK = ( {
const [ optInDataSharing, setIsOptInDataSharing ] =
useState< boolean >( true );
+ const [ isFetchingPatterns, setIsFetchingPatterns ] = useState( false );
+
const optIn = () => {
trackEvent(
'customize_your_store_assembler_hub_opt_in_usage_tracking'
@@ -200,6 +235,10 @@ export const SidebarNavigationScreenHomepagePTK = ( {
navigateTo( {
url: categoryUrl,
} );
+ trackEvent(
+ 'customize_your_store_assembler_pattern_category_click',
+ { category: categoryKey }
+ );
} }
as={ SidebarNavigationItem }
withChevron
@@ -241,6 +280,19 @@ export const SidebarNavigationScreenHomepagePTK = ( {
variant="link"
/>
),
+ FetchPatterns: (
+ {
+ await apiFetch( {
+ path: `/wc/private/patterns`,
+ method: 'POST',
+ } );
+
+ invalidateCache();
+ } }
+ variant="link"
+ />
+ ),
} ) }
{ isModalOpen && (
@@ -249,7 +301,7 @@ export const SidebarNavigationScreenHomepagePTK = ( {
'woocommerce-customize-store__opt-in-usage-tracking-modal'
}
title={ __(
- 'Opt in to usage tracking',
+ 'Access more patterns',
'woocommerce'
) }
onRequestClose={ closeModal }
@@ -259,7 +311,7 @@ export const SidebarNavigationScreenHomepagePTK = ( {
className="core-profiler__checkbox"
label={ interpolateComponents( {
mixedString: __(
- 'I agree to share my data to tailor my store setup experience, get more relevant content, and help make WooCommerce better for everyone. You can opt out at any time in WooCommerce settings. {{link}}Learn more about usage tracking{{/link}}.',
+ 'More patterns from the WooCommerce.com library are available! Opt in to connect your store and access the full library, plus get more relevant content and a tailored store setup experience. Opting in will enable {{link}}usage tracking{{/link}}, which you can opt out of at any time via WooCommerce settings.',
'woocommerce'
),
components: {
@@ -289,24 +341,34 @@ export const SidebarNavigationScreenHomepagePTK = ( {
) }
{
+ onClick={ async () => {
optIn();
- if ( isIframe( window ) ) {
- sendMessageToParent( {
- type: 'INSTALL_PATTERNS',
- } );
- } else {
- sendEvent(
- 'INSTALL_PATTERNS'
- );
- }
+ await enableTracking();
+ setIsFetchingPatterns(
+ true
+ );
+ await apiFetch< {
+ success: boolean;
+ } >( {
+ path: `/wc/private/patterns`,
+ method: 'POST',
+ } );
+ invalidateCache();
+ closeModal();
+ setIsFetchingPatterns(
+ false
+ );
} }
variant="primary"
disabled={ ! optInDataSharing }
>
- { __(
- 'Opt in',
- 'woocommerce'
+ { isFetchingPatterns ? (
+
+ ) : (
+ __(
+ 'Opt in',
+ 'woocommerce'
+ )
) }
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage-ptk/sidebar-navigation-screen-homepage.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage/sidebar-navigation-screen-homepage.tsx
similarity index 62%
rename from plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage-ptk/sidebar-navigation-screen-homepage.tsx
rename to plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage/sidebar-navigation-screen-homepage.tsx
index 18c0fa1400b..684819072a9 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage-ptk/sidebar-navigation-screen-homepage.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage/sidebar-navigation-screen-homepage.tsx
@@ -10,10 +10,9 @@ import {
useMemo,
useEffect,
useContext,
- useState,
} from '@wordpress/element';
import { Link } from '@woocommerce/components';
-import { Spinner, Button, Modal, CheckboxControl } from '@wordpress/components';
+import { Spinner } from '@wordpress/components';
// @ts-expect-error Missing type.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-expect-error No types for this exist yet.
@@ -25,7 +24,6 @@ import {
} from '@wordpress/block-editor';
// @ts-expect-error Missing type in core-data.
import { useIsSiteEditorLoading } from '@wordpress/edit-site/build-module/components/layout/hooks';
-import interpolateComponents from '@automattic/interpolate-components';
/**
* Internal dependencies
@@ -44,13 +42,10 @@ import { select, useSelect } from '@wordpress/data';
import { trackEvent } from '~/customize-store/tracking';
import {
PRODUCT_HERO_PATTERN_BUTTON_STYLE,
- findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate,
-} from '../../utils/hero-pattern';
-import { useNetworkStatus } from '~/utils/react-hooks/use-network-status';
-import { isIframe, sendMessageToParent } from '~/customize-store/utils';
-import { isTrackingAllowed } from '../../utils/is-tracking-allowed';
-import './style.scss';
+ findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate,
+} from '../../utils/black-background-pattern-update-button';
import { useIsActiveNewNeutralVariation } from '../../hooks/use-is-active-new-neutral-variation';
+import './style.scss';
export const SidebarNavigationScreenHomepage = ( {
onNavigateBackClick,
@@ -116,14 +111,33 @@ export const SidebarNavigationScreenHomepage = ( {
}
if ( ! isActiveNewNeutralVariation ) {
- return [ ...acc, ...parsedPattern.blocks ];
+ const updatedBlocks =
+ findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate(
+ parsedPattern.blocks,
+ (
+ buttonBlocks: BlockInstance[]
+ ) => {
+ buttonBlocks.forEach(
+ ( buttonBlock ) => {
+ buttonBlock.attributes.style =
+ {};
+ }
+ );
+ }
+ );
+
+ return [ ...acc, ...updatedBlocks ];
}
const updatedBlocks =
- findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate(
+ findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate(
parsedPattern.blocks,
- ( buttonBlock: BlockInstance ) => {
- buttonBlock.attributes.style =
- PRODUCT_HERO_PATTERN_BUTTON_STYLE;
+ ( buttonBlocks: BlockInstance[] ) => {
+ buttonBlocks.forEach(
+ ( buttonBlock ) => {
+ buttonBlock.attributes.style =
+ PRODUCT_HERO_PATTERN_BUTTON_STYLE;
+ }
+ );
}
);
@@ -195,7 +209,7 @@ export const SidebarNavigationScreenHomepage = ( {
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to re-run this effect when currentSelectedPattern changes
}, [ blocks, homePatterns, isLoading, isEditorLoading ] );
- const { context, sendEvent } = useContext( CustomizeStoreContext );
+ const { context } = useContext( CustomizeStoreContext );
const aiOnline = context.flowType === FlowType.AIOnline;
const title = aiOnline
@@ -211,47 +225,6 @@ export const SidebarNavigationScreenHomepage = ( {
'woocommerce'
);
- const isNetworkOffline = useNetworkStatus();
- const isPTKPatternsAPIAvailable = context.isPTKPatternsAPIAvailable;
-
- let notice;
- if ( isNetworkOffline ) {
- notice = __(
- "Looks like we can't detect your network. Please double-check your internet connection and refresh the page.",
- 'woocommerce'
- );
- } else if ( ! isPTKPatternsAPIAvailable ) {
- notice = __(
- "Unfortunately, we're experiencing some technical issues — please come back later to access more patterns.",
- 'woocommerce'
- );
- } else if ( ! isTrackingAllowed() ) {
- notice = __(
- 'Opt in to usage tracking to get access to more patterns.',
- 'woocommerce'
- );
- }
-
- const [ isModalOpen, setIsModalOpen ] = useState( false );
-
- const openModal = () => setIsModalOpen( true );
- const closeModal = () => setIsModalOpen( false );
-
- const [ optInDataSharing, setIsOptInDataSharing ] =
- useState< boolean >( true );
-
- const optIn = () => {
- trackEvent(
- 'customize_your_store_assembler_hub_opt_in_usage_tracking'
- );
- };
-
- const skipOptIn = () => {
- trackEvent(
- 'customize_your_store_assembler_hub_skip_opt_in_usage_tracking'
- );
- };
-
return (
) }
- { notice && (
-
-
- { __(
- 'Want more patterns?',
- 'woocommerce'
- ) }
-
-
- { createInterpolateElement( notice, {
- OptInModal: (
- {
- openModal();
- } }
- variant="link"
- />
- ),
- } ) }
-
- { isModalOpen && (
-
-
- ),
- },
- } ) }
- checked={ optInDataSharing }
- onChange={ setIsOptInDataSharing }
- />
-
- {
- skipOptIn();
- closeModal();
- } }
- variant="link"
- >
- { __(
- 'Cancel',
- 'woocommerce'
- ) }
-
- {
- optIn();
- if ( isIframe( window ) ) {
- sendMessageToParent( {
- type: 'INSTALL_PATTERNS',
- } );
- } else {
- sendEvent(
- 'INSTALL_PATTERNS'
- );
- }
- } }
- variant="primary"
- disabled={ ! optInDataSharing }
- >
- { __(
- 'Opt in',
- 'woocommerce'
- ) }
-
-
-
- ) }
-
- ) }
}
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage/style.scss b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage/style.scss
new file mode 100644
index 00000000000..cb7e0429b37
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage/style.scss
@@ -0,0 +1,34 @@
+.woocommerce-customize-store__sidebar-homepage-content {
+ .block-editor-block-patterns-list__item {
+ .auto-block-preview__container,
+ .block-editor-block-preview__container {
+ border: none;
+ }
+
+ &.is-selected {
+ .auto-block-preview__container,
+ .block-editor-block-preview__container {
+ box-shadow: 0 0 0 0 #fff,
+ 0 0 0 2px var(--wp-admin-theme-color, #3858e9);
+ border-color: var(--wp-admin-theme-color);
+ border-radius: 4px;
+ &::after {
+ outline: none;
+ }
+ }
+ }
+
+ &:hover {
+ .auto-block-preview__container,
+ .block-editor-block-preview__container {
+ box-shadow: 0 0 0 0 #fff,
+ 0 0 0 2px var(--wp-admin-theme-color, #3858e9);
+ border-color: var(--wp-admin-theme-color);
+ border-radius: 4px;
+ &::after {
+ outline: none;
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-logo.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-logo.tsx
index 3ed67ea316f..e996aa0f489 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-logo.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-logo.tsx
@@ -49,6 +49,7 @@ import {
MAX_LOGO_WIDTH,
ALLOWED_MEDIA_TYPES,
} from './constants';
+import { trackEvent } from '~/customize-store/tracking';
const useLogoEdit = ( {
shouldSyncIcon,
@@ -541,9 +542,15 @@ export const SidebarNavigationScreenLogo = ( {
components: {
link: (
{
+ trackEvent(
+ 'customize_your_store_fiverr_logo_maker_cta_click'
+ );
+ } }
/>
),
},
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography/sidebar-navigation-screen-typography.tsx
similarity index 83%
rename from plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography.tsx
rename to plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography/sidebar-navigation-screen-typography.tsx
index 27c4830b560..88f66bc0caa 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography/sidebar-navigation-screen-typography.tsx
@@ -12,26 +12,27 @@ import {
import { useSelect } from '@wordpress/data';
import { Link } from '@woocommerce/components';
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
-import { Button, Modal, CheckboxControl } from '@wordpress/components';
+import { Button, Modal, CheckboxControl, Spinner } from '@wordpress/components';
import interpolateComponents from '@automattic/interpolate-components';
/**
* Internal dependencies
*/
-import { SidebarNavigationScreen } from './sidebar-navigation-screen';
+import { SidebarNavigationScreen } from '../sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
-import { FontPairing } from './global-styles';
-import { CustomizeStoreContext } from '..';
+import { FontPairing } from '../global-styles';
+import { CustomizeStoreContext } from '../..';
import { FlowType } from '~/customize-store/types';
-import { isIframe, sendMessageToParent } from '~/customize-store/utils';
import { trackEvent } from '~/customize-store/tracking';
+import { installFontFamilies } from '../../utils/fonts';
+import { enableTracking } from '~/customize-store/design-without-ai/services';
export const SidebarNavigationScreenTypography = ( {
onNavigateBackClick,
}: {
onNavigateBackClick: () => void;
} ) => {
- const { context, sendEvent } = useContext( CustomizeStoreContext );
+ const { context } = useContext( CustomizeStoreContext );
const aiOnline = context.flowType === FlowType.AIOnline;
const isFontLibraryAvailable = context.isFontLibraryAvailable;
@@ -90,6 +91,8 @@ export const SidebarNavigationScreenTypography = ( {
const openModal = () => setIsModalOpen( true );
const closeModal = () => setIsModalOpen( false );
+ const [ isFetchingFonts, setIsFetchingFonts ] = useState( false );
+
const [ OptInDataSharing, setIsOptInDataSharing ] =
useState< boolean >( true );
@@ -170,7 +173,7 @@ export const SidebarNavigationScreenTypography = ( {
'woocommerce-customize-store__opt-in-usage-tracking-modal'
}
title={ __(
- 'Get more fonts',
+ 'Access more fonts',
'woocommerce'
) }
onRequestClose={ closeModal }
@@ -180,7 +183,7 @@ export const SidebarNavigationScreenTypography = ( {
className="core-profiler__checkbox"
label={ interpolateComponents( {
mixedString: __(
- 'I would like to get store updates, including new fonts, on an ongoing basis. In doing so, I agree to share my data to tailor my store setup experience, get more relevant content, and help make WooCommerce better for everyone. You can opt out at any time in WooCommerce settings. {{link}}Learn more about usage tracking{{/link}}.',
+ 'More fonts are available! Opt in to connect your store and access the full font library, plus get more relevant content and a tailored store setup experience. Opting in will enable {{link}}usage tracking{{/link}}, which you can opt out of at any time via WooCommerece settings.',
'woocommerce'
),
components: {
@@ -207,22 +210,23 @@ export const SidebarNavigationScreenTypography = ( {
{ __( 'Cancel', 'woocommerce' ) }
{
+ onClick={ async () => {
optIn();
- if ( isIframe( window ) ) {
- sendMessageToParent( {
- type: 'INSTALL_FONTS',
- } );
- } else {
- sendEvent(
- 'INSTALL_FONTS'
- );
- }
+ setIsFetchingFonts( true );
+ await enableTracking();
+ await installFontFamilies();
+
+ closeModal();
+ setIsFetchingFonts( false );
} }
variant="primary"
disabled={ ! OptInDataSharing }
>
- { __( 'Opt in', 'woocommerce' ) }
+ { isFetchingFonts ? (
+
+ ) : (
+ __( 'Opt in', 'woocommerce' )
+ ) }
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/utils.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/utils.ts
index bd4575221f8..dcc27523da6 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/utils.ts
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/utils.ts
@@ -2,7 +2,6 @@
* External dependencies
*/
import { BlockInstance } from '@wordpress/blocks';
-import { isEqual } from 'lodash';
/**
* Internal dependencies
@@ -24,12 +23,12 @@ export const findPatternByBlock = (
''
);
}
+
return patterns.find( ( pattern ) => {
const patternBlocks = pattern.blocks[ 0 ];
- if ( patternBlocks.innerBlocks.length !== block.innerBlocks.length ) {
- return false;
- }
- return isEqual( patternBlocks.attributes, blockAttributes );
+ return (
+ patternBlocks.attributes.className === block.attributes.className
+ );
} );
};
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss b/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss
index db48d7f7bf6..2bd027500f9 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss
@@ -38,7 +38,7 @@ body.woocommerce-assembler {
position: fixed;
right: 0;
top: 0;
- background-color: #fcfcfc;
+ background-color: #f5f5f5;
}
/* Sidebar Header */
@@ -67,7 +67,7 @@ body.woocommerce-assembler {
.edit-site-sidebar-navigation-screen__title-icon,
.edit-site-site-hub__view-mode-toggle-container,
.edit-site-layout__view-mode-toggle-icon.edit-site-site-icon {
- background-color: #fcfcfc;
+ background-color: #f5f5f5;
}
.edit-site-layout__view-mode-toggle-icon.edit-site-site-icon {
@@ -101,7 +101,7 @@ body.woocommerce-assembler {
.edit-site-layout__sidebar-region {
width: 380px;
z-index: 3;
- background-color: #fafafa;
+ background-color: #f5f5f5;
}
.edit-site-layout__sidebar {
@@ -170,14 +170,14 @@ body.woocommerce-assembler {
align-items: center;
gap: 8px;
align-self: stretch;
- width: 348px;
+ width: 100%;
border: 1.5px solid transparent;
&:hover,
&:active,
&:focus,
&.edit-site-sidebar-navigation-screen-patterns__group-homepage-item--active {
- background: #ededed;
+ background: #e4e4e4;
color: $gray-600;
}
@@ -191,7 +191,8 @@ body.woocommerce-assembler {
}
}
- .edit-site-sidebar-navigation-item.components-item .edit-site-sidebar-navigation-item__drilldown-indicator {
+ .edit-site-sidebar-navigation-item.components-item
+ .edit-site-sidebar-navigation-item__drilldown-indicator {
fill: #ccc;
}
@@ -235,7 +236,7 @@ body.woocommerce-assembler {
gap: $gap;
align-self: stretch;
border-radius: 4px;
- background: #f0f0f0;
+ background: #e4e4e4;
width: 324px;
height: 88px;
@@ -484,8 +485,6 @@ body.woocommerce-assembler {
}
}
- /* Layout sidebar */
-
.block-editor-block-preview__content {
height: 100%;
width: 100%;
@@ -505,6 +504,7 @@ body.woocommerce-assembler {
.block-editor-block-patterns-list {
width: 324px;
+ overflow-y: unset;
}
}
@@ -519,7 +519,7 @@ body.woocommerce-assembler {
.edit-site-layout__canvas {
bottom: 16px;
top: 16px;
- left: 12px; // the default styles for this undersizes the width by 24px so we want to center this
+ left: 6px; // the default styles for this undersizes the width by 24px so we want to center this
padding: 0 4px 0 16px;
}
@@ -529,15 +529,16 @@ body.woocommerce-assembler {
}
.edit-site-layout__canvas .components-resizable-box__container {
- border-radius: 20px;
- box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.25),
- 0 6px 10px 0 rgba(0, 0, 0, 0.02), 0 13px 15px 0 rgba(0, 0, 0, 0.03),
- 0 15px 20px 0 rgba(0, 0, 0, 0.04);
+ border-radius: 12px;
+ /* new frame */
+ box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.05);
}
.woocommerce-customize-store__block-editor,
- .edit-site-layout:not(.is-full-canvas) .edit-site-layout__canvas > div .interface-interface-skeleton__content {
- border-radius: 20px;
+ .edit-site-layout:not(.is-full-canvas)
+ .edit-site-layout__canvas > div
+ .interface-interface-skeleton__content {
+ border-radius: 12px;
.woocommerce-customize-store__block-editor,
.woocommerce-block-preview-container,
@@ -554,7 +555,7 @@ body.woocommerce-assembler {
.customize-your-store-edit-site-resizable-frame__inner-content {
height: 100%;
- border-radius: 20px !important;
+ border-radius: 12px;
}
}
@@ -602,6 +603,7 @@ body.woocommerce-assembler {
& > .tour-kit-frame__arrow {
right: -5px;
&::before {
+ background: #fff;
border-top: 1px solid var(--gutenberg-gray-300, #ddd);
border-right: 1px solid var(--gutenberg-gray-300, #ddd);
}
@@ -613,6 +615,7 @@ body.woocommerce-assembler {
&::before {
border-bottom: 1px solid var(--gutenberg-gray-300, #ddd);
border-left: 1px solid var(--gutenberg-gray-300, #ddd);
+ background: #fff;
}
}
}
@@ -757,7 +760,8 @@ body.woocommerce-assembler {
justify-content: space-between;
}
-.components-dropdown-menu__menu .woocommerce-customize-store__logo-menu-group.components-menu-group {
+.components-dropdown-menu__menu
+.woocommerce-customize-store__logo-menu-group.components-menu-group {
border-top: 0;
padding: 0 8px;
@@ -804,4 +808,7 @@ body.woocommerce-assembler {
display: flex;
max-width: 700px;
}
+ .edit-site-layout__canvas-container {
+ background-color: #f5f5f5;
+ }
}
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/toolbar/delete.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/toolbar/delete.tsx
index fe76522101a..b665a5ed6a8 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/toolbar/delete.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/toolbar/delete.tsx
@@ -10,11 +10,18 @@ import {
// @ts-expect-error missing type
} from '@wordpress/block-editor';
+/**
+ * Internal dependencies
+ */
+import { trackEvent } from '~/customize-store/tracking';
+
export default function Delete( {
clientId,
+ currentBlockName,
nextBlockClientId,
}: {
clientId: string;
+ currentBlockName: string | undefined;
nextBlockClientId: string | undefined;
} ) {
// @ts-expect-error missing type
@@ -24,13 +31,17 @@ export default function Delete( {
{
removeBlock( clientId );
if ( nextBlockClientId ) {
selectBlock( nextBlockClientId );
}
+ trackEvent(
+ 'customize_your_store_assembler_pattern_delete_click',
+ { pattern: currentBlockName }
+ );
} }
/>
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/toolbar/shuffle.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/toolbar/shuffle.tsx
index 222ec22c493..14c28d0516b 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/toolbar/shuffle.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/toolbar/shuffle.tsx
@@ -16,6 +16,7 @@ import {
import { PatternWithBlocks } from '~/customize-store/types/pattern';
import { PATTERN_CATEGORIES } from '../sidebar/pattern-screen/categories';
import { usePatternsByCategory } from '../hooks/use-patterns';
+import { trackEvent } from '~/customize-store/tracking';
// This is the icon that is used in the Shuffle button. Currently we are using an outdated version of @wordpress/icons.
// import { shuffle } from '@wordpress/icons';
@@ -105,7 +106,8 @@ export default function Shuffle( { clientId }: { clientId: string } ) {
// @ts-expect-error missing type
const { replaceBlocks } = useDispatch( blockEditorStore );
- if ( patterns.length === 0 ) {
+ // We need at least two patterns to shuffle.
+ if ( patterns.length < 2 ) {
return null;
}
@@ -128,6 +130,10 @@ export default function Shuffle( { clientId }: { clientId: string } ) {
},
};
replaceBlocks( clientId, nextPattern.blocks );
+ trackEvent(
+ 'customize_your_store_assembler_pattern_shuffle_click',
+ { category, pattern: nextPattern.name }
+ );
} }
>
{ categoryLabel && (
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/toolbar/toolbar.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/toolbar/toolbar.tsx
index 8bb77163f5e..1d0a737f2a8 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/toolbar/toolbar.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/toolbar/toolbar.tsx
@@ -214,6 +214,7 @@ export const Toolbar = () => {
>
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/tracking/index.js b/plugins/woocommerce-admin/client/customize-store/assembler-hub/tracking/index.js
new file mode 100644
index 00000000000..4c25ddc2fdc
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/tracking/index.js
@@ -0,0 +1,97 @@
+// Partially copied from: https://github.com/Automattic/wp-calypso/blob/6241a94e52e60a662245bdb06ecae9c724bcec5f/apps/wpcom-block-editor/src/wpcom/features/tracking.js
+
+/**
+ * External dependencies
+ */
+import { use } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { trackEvent } from '~/customize-store/tracking';
+
+const rewrittenActions = {};
+const originalActions = {};
+
+/**
+ * Tracker can be
+ * - string - which means it is an event name and should be tracked as such automatically
+ * - function - in case you need to load additional properties from the action.
+ *
+ * @type {Object}
+ */
+const REDUX_TRACKING = {
+ 'core/block-editor': {
+ moveBlocksUp: () =>
+ trackEvent(
+ 'customize_your_store_assembler_pattern_move_up_click'
+ ),
+ moveBlocksDown: () =>
+ trackEvent(
+ 'customize_your_store_assembler_pattern_move_down_click'
+ ),
+ },
+};
+
+use(
+ ( registry ) => ( {
+ dispatch: ( namespace ) => {
+ const namespaceName =
+ typeof namespace === 'object' ? namespace.name : namespace;
+ const actions = { ...registry.dispatch( namespaceName ) };
+
+ const trackers = REDUX_TRACKING[ namespaceName ];
+
+ // Initialize namespace level objects if not yet done.
+ if ( ! rewrittenActions[ namespaceName ] ) {
+ rewrittenActions[ namespaceName ] = {};
+ }
+ if ( ! originalActions[ namespaceName ] ) {
+ originalActions[ namespaceName ] = {};
+ }
+
+ if ( trackers ) {
+ Object.keys( trackers ).forEach( ( actionName ) => {
+ const originalAction = actions[ actionName ];
+ const tracker = trackers[ actionName ];
+ // If we havent stored the originalAction, or it is no longer the same as the
+ // one we last wrote a corresponding rewrittenAction for, we need to update.
+ if (
+ ! originalActions[ namespaceName ][ actionName ] ||
+ originalActions[ namespaceName ][ actionName ] !==
+ originalAction
+ ) {
+ // Save the originalAction and rewrittenAction for future reference.
+ originalActions[ namespaceName ][ actionName ] =
+ originalAction;
+ rewrittenActions[ namespaceName ][ actionName ] = (
+ ...args
+ ) => {
+ // We use a try-catch here to make sure the `originalAction`
+ // is always called. We don't want to break the original
+ // behaviour when our tracking throws an error.
+ try {
+ if ( typeof tracker === 'string' ) {
+ // Simple track - just based on the event name.
+ trackEvent( tracker );
+ } else if ( typeof tracker === 'function' ) {
+ // Advanced tracking - call function.
+ tracker( ...args );
+ }
+ } catch ( err ) {
+ // eslint-disable-next-line no-console
+ console.error( err );
+ }
+ return originalAction( ...args );
+ };
+ }
+ // Replace the action in the registry with the rewrittenAction.
+ actions[ actionName ] =
+ rewrittenActions[ namespaceName ][ actionName ];
+ } );
+ }
+ return actions;
+ },
+ } ),
+ {}
+);
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/black-background-pattern-update-button.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/black-background-pattern-update-button.ts
new file mode 100644
index 00000000000..e7251059f2d
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/black-background-pattern-update-button.ts
@@ -0,0 +1,97 @@
+/**
+ * External dependencies
+ */
+import { BlockInstance } from '@wordpress/blocks';
+
+const updateFeaturedCategoryCoverImagePattern = (
+ featuredCategoryCoverImagePatternParentBlocks: BlockInstance[],
+ callback: ( buttonBlocks: BlockInstance[] ) => void
+) => {
+ const coverBlocks = featuredCategoryCoverImagePatternParentBlocks.map(
+ ( featuredCategoryCoverImagePatternParentBlock ) =>
+ featuredCategoryCoverImagePatternParentBlock.innerBlocks.find(
+ ( block ) => block.name === 'core/cover'
+ )
+ );
+
+ const parentButtonBlocks = coverBlocks.map( ( coverBlock ) =>
+ coverBlock?.innerBlocks.find(
+ ( block ) => block.name === 'core/buttons'
+ )
+ );
+
+ const buttonBlocks = parentButtonBlocks.map(
+ ( parentButtonBlock ) => parentButtonBlock?.innerBlocks[ 0 ]
+ );
+
+ callback( buttonBlocks as BlockInstance[] );
+};
+
+const updateJustArrivedFullHeroPattern = (
+ justArrivedFullHeroPatterns: BlockInstance[],
+ callback: ( buttonBlocks: BlockInstance[] ) => void
+) => {
+ const parentButtonBlocks = justArrivedFullHeroPatterns.map(
+ ( justArrivedFullHeroPattern ) =>
+ justArrivedFullHeroPattern?.innerBlocks[ 0 ].innerBlocks.find(
+ ( block ) => block.name === 'core/buttons'
+ )
+ );
+
+ const buttonBlocks = parentButtonBlocks
+ .map( ( parentButtonBlock ) => parentButtonBlock?.innerBlocks[ 0 ] )
+ .filter( Boolean );
+
+ if ( ! buttonBlocks ) {
+ return;
+ }
+
+ callback( buttonBlocks as BlockInstance[] );
+};
+
+export const isJustArrivedFullHeroPattern = ( block: BlockInstance ) =>
+ block.name === 'core/cover' &&
+ block.attributes.url.includes(
+ 'music-black-and-white-white-photography.jpg'
+ );
+
+export const isFeaturedCategoryCoverImagePattern = ( block: BlockInstance ) =>
+ block.attributes?.metadata?.name === 'Featured Category Cover Image';
+
+/**
+ * This is temporary solution to change the button color on the cover block when the color palette is New - Neutral.
+ * The real fix should be done on Gutenberg side: https://github.com/WordPress/gutenberg/issues/58004
+ *
+ */
+export const findButtonBlockInsideCoverBlockWithBlackBackgroundPatternAndUpdate =
+ (
+ blocks: BlockInstance[],
+ callback: ( buttonBlocks: BlockInstance[] ) => void
+ ) => {
+ const justArrivedFullHeroPatterns = blocks.filter(
+ isJustArrivedFullHeroPattern
+ );
+
+ if ( justArrivedFullHeroPatterns ) {
+ updateJustArrivedFullHeroPattern(
+ justArrivedFullHeroPatterns,
+ callback
+ );
+ }
+
+ const featuredCategoryCoverImagePatterns = blocks.filter(
+ isFeaturedCategoryCoverImagePattern
+ );
+
+ if ( featuredCategoryCoverImagePatterns ) {
+ updateFeaturedCategoryCoverImagePattern(
+ featuredCategoryCoverImagePatterns,
+ callback
+ );
+ }
+ return blocks;
+ };
+
+export const PRODUCT_HERO_PATTERN_BUTTON_STYLE = {
+ color: { background: '#ffffff', text: '#000000' },
+};
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/fonts.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/fonts.ts
new file mode 100644
index 00000000000..b36ec449693
--- /dev/null
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/fonts.ts
@@ -0,0 +1,282 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { resolveSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import {
+ FontFace,
+ FontFamiliesToInstall,
+ FontFamily,
+} from '~/customize-store/types/font';
+import { FONT_FAMILIES_TO_INSTALL } from '../sidebar/global-styles/font-pairing-variations/constants';
+
+export type FontCollectionsResponse = Array< {
+ slug: string;
+ description: string;
+ name: string;
+} >;
+
+export type FontCollectionResponse = {
+ slug: string;
+ name: string;
+ font_families: Array< {
+ font_family_settings: FontFamily;
+ categories: Array< string >;
+ } >;
+};
+
+const getInstalledFontFamilyByNameFontFamily = (
+ installedFontFamilies: Array< {
+ id: number;
+ font_family_settings: FontFamily;
+ font_face: Array< FontFace >;
+ } >,
+ nameFontFamily: string
+) => {
+ return installedFontFamilies.find(
+ ( { font_family_settings } ) =>
+ font_family_settings.slug === nameFontFamily
+ );
+};
+
+const getFontFamiliesToInstall = (
+ fontCollection: FontCollectionResponse,
+ slugFontFamily: string,
+ fontFamilyToInstall: FontFamiliesToInstall[ 'slug' ]
+) => {
+ const fontFromCollection = fontCollection.font_families.find(
+ ( { font_family_settings } ) =>
+ font_family_settings.slug === slugFontFamily
+ );
+
+ if ( ! fontFromCollection ) {
+ return null;
+ }
+
+ const fontFace = fontFromCollection?.font_family_settings.fontFace.filter(
+ ( { fontWeight } ) =>
+ fontFamilyToInstall.fontWeights.includes( fontWeight )
+ );
+
+ const fontFamilyWithFontFace = {
+ ...fontFromCollection?.font_family_settings,
+ fontFace,
+ };
+
+ return fontFamilyWithFontFace;
+};
+
+/**
+ * Retrieves font families and font faces to install based on a provided font collection and a list of installed font families.
+ * The fontFamilyWithFontFaceToInstall include fontFamilies with font faces that are not installed yet.
+ * The fontFaceToInstall include font faces that are not installed yet, but already have the font family installed.
+ *
+ * @param fontCollection - The complete font collection containing all available font data.
+ * @param installedFontFamilies - An array of installed font families with associated font faces and settings.
+ * @return An object containing font families with font faces to install and individual font faces to install.
+ */
+export const getFontFamiliesAndFontFaceToInstall = (
+ fontCollection: FontCollectionResponse,
+ installedFontFamilies: Array< {
+ id: number;
+ font_face: Array< FontFace >;
+ font_family_settings: FontFamily;
+ } >
+) => {
+ return Object.entries( FONT_FAMILIES_TO_INSTALL ).reduce(
+ ( acc, [ slug, fontData ] ) => {
+ const fontFamilyWithFontFaceToInstall = getFontFamiliesToInstall(
+ fontCollection,
+ slug,
+ fontData
+ );
+
+ if ( ! fontFamilyWithFontFaceToInstall ) {
+ return acc;
+ }
+
+ const fontFamily = getInstalledFontFamilyByNameFontFamily(
+ installedFontFamilies,
+ fontFamilyWithFontFaceToInstall.slug
+ );
+
+ if ( ! fontFamily ) {
+ return {
+ ...acc,
+ fontFamiliesWithFontFacesToInstall: [
+ ...acc.fontFamiliesWithFontFacesToInstall,
+ fontFamilyWithFontFaceToInstall,
+ ],
+ };
+ }
+
+ const fontFace = fontFamily.font_face.filter( ( { fontWeight } ) =>
+ fontData.fontWeights.includes( fontWeight )
+ );
+
+ return {
+ ...acc,
+ fontFacesToInstall: [
+ ...acc.fontFacesToInstall,
+ ...fontFace.map( ( face ) => ( {
+ ...face,
+ fontFamilyId: fontFamily.id,
+ } ) ),
+ ],
+ };
+ },
+ {
+ fontFamiliesWithFontFacesToInstall: [],
+ fontFacesToInstall: [],
+ } as {
+ fontFamiliesWithFontFacesToInstall: Array< FontFamily >;
+ fontFacesToInstall: Array<
+ FontFace & {
+ fontFamilyId: number;
+ }
+ >;
+ }
+ );
+};
+
+export const installFontFamily = ( data: FontFamily ) => {
+ const config = {
+ path: '/wp/v2/font-families',
+ method: 'POST',
+ data: {
+ font_family_settings: JSON.stringify( {
+ name: data.name,
+ slug: data.slug,
+ fontFamily: data.fontFamily,
+ preview: data.preview,
+ } ),
+ },
+ };
+
+ return apiFetch< {
+ id: number;
+ font_family_settings: string;
+ } >( config );
+};
+
+async function downloadFontFaceAssets( src: string ) {
+ try {
+ const fontBlob = await ( await fetch( new Request( src ) ) ).blob();
+ const fileName = src.split( '/' ).pop() as string;
+ return new File( [ fontBlob ], fileName, {
+ type: fontBlob.type,
+ } );
+ } catch ( error ) {
+ throw new Error( `Error downloading font face asset from ${ src }` );
+ }
+}
+
+function makeFontFacesFormData(
+ fontFaceFile: File,
+ formData: FormData,
+ index: number
+) {
+ const fileId = `file-${ index }`;
+ formData.append( fileId, fontFaceFile, fontFaceFile.name );
+ return fileId;
+}
+
+export const installFontFace = async (
+ data: FontFace & {
+ fontFamilyId: number;
+ },
+ index: number
+) => {
+ const { fontFamilyId, ...font } = data;
+ const fontFaceAssets = await downloadFontFaceAssets(
+ Array.isArray( font.src ) ? font.src[ 0 ] : font.src
+ );
+ const formData = new FormData();
+
+ const fontFile = await makeFontFacesFormData(
+ fontFaceAssets,
+ formData,
+ index
+ );
+
+ formData.append(
+ 'font_face_settings',
+ JSON.stringify( { ...font, src: fontFile } )
+ );
+ const config = {
+ path: `/wp/v2/font-families/${ data.fontFamilyId }/font-faces/`,
+ method: 'POST',
+ body: formData,
+ };
+
+ return apiFetch( config );
+};
+
+export const installFontFamilies = async () => {
+ const installedFontFamily = ( await resolveSelect(
+ 'core'
+ ).getEntityRecords( 'postType', 'wp_font_family', {
+ per_page: -1,
+ } ) ) as Array< {
+ id: number;
+ font_faces: Array< number >;
+ font_family_settings: FontFamily;
+ } >;
+
+ const installedFontFamiliesWithFontFaces = await Promise.all(
+ installedFontFamily.map( async ( fontFamily ) => {
+ const fontFaces = await apiFetch< Array< FontFace > >( {
+ path: `/wp/v2/font-families/${ fontFamily.id }/font-faces`,
+ method: 'GET',
+ } );
+
+ return {
+ ...fontFamily,
+ font_face: fontFaces,
+ };
+ } )
+ );
+
+ const fontCollection = await apiFetch< FontCollectionResponse >( {
+ path: `/wp/v2/font-collections/google-fonts`,
+ method: 'GET',
+ } );
+
+ const { fontFacesToInstall, fontFamiliesWithFontFacesToInstall } =
+ getFontFamiliesAndFontFaceToInstall(
+ fontCollection,
+ installedFontFamiliesWithFontFaces
+ );
+
+ const fontFamiliesWithFontFaceToInstallPromises =
+ fontFamiliesWithFontFacesToInstall.map( async ( fontFamily ) => {
+ const fontFamilyResponse = await installFontFamily( fontFamily );
+ return Promise.all(
+ fontFamily.fontFace.map( async ( fontFace, index ) => {
+ return installFontFace(
+ {
+ ...fontFace,
+ fontFamilyId: fontFamilyResponse.id,
+ },
+ index
+ );
+ } )
+ );
+ } );
+
+ const fontFacesToInstallPromises =
+ fontFacesToInstall.map( installFontFace );
+
+ return ( await Promise.all( [
+ ...fontFamiliesWithFontFaceToInstallPromises,
+ ...fontFacesToInstallPromises,
+ ] ) ) as Array<
+ Array< {
+ font_face_settings: FontFace;
+ } >
+ >;
+};
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/hero-pattern.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/hero-pattern.ts
deleted file mode 100644
index d718ccea41b..00000000000
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/hero-pattern.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * External dependencies
- */
-import { BlockInstance } from '@wordpress/blocks';
-
-/**
- * This is temporary solution to change the button color on the cover block when the color palette is New - Neutral.
- * The real fix should be done on Gutenberg side: https://github.com/WordPress/gutenberg/issues/58004
- *
- */
-export const findButtonBlockInsideCoverBlockProductHeroPatternAndUpdate = (
- blocks: BlockInstance[],
- callback: ( buttonBlock: BlockInstance ) => void
-) => {
- const coverBlock = blocks.find(
- ( block ) =>
- block.name === 'core/cover' &&
- block.attributes.url.includes(
- 'music-black-and-white-white-photography.jpg'
- )
- );
-
- const buttonsBlock = coverBlock?.innerBlocks[ 0 ].innerBlocks.find(
- ( block ) => block.name === 'core/buttons'
- );
-
- const buttonBlock = buttonsBlock?.innerBlocks[ 0 ];
-
- if ( ! buttonBlock ) {
- return blocks;
- }
-
- callback( buttonBlock );
- return blocks;
-};
-
-export const PRODUCT_HERO_PATTERN_BUTTON_STYLE = {
- color: { background: '#ffffff', text: '#000000' },
-};
diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/is-full-composability-enabled.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/is-full-composability-enabled.ts
index 0c082292b5a..aa330f685fb 100644
--- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/is-full-composability-enabled.ts
+++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/utils/is-full-composability-enabled.ts
@@ -6,17 +6,27 @@ import {
// @ts-expect-error No types for this exist yet.
} from '@wordpress/block-editor';
-export const isPatternToolkitFullComposabilityFeatureFlagEnabled = () => {
+const isPatternToolkitFullComposabilityFeatureFlagEnabled = () => {
+ // @ts-expect-error temp fix
+ if ( window.parent?.window.cys_aiFlow ) {
+ return false;
+ }
+
return window.wcAdminFeatures[ 'pattern-toolkit-full-composability' ];
};
-export const isGutenbergAPIAvailableForFullComposability = () => {
+const isGutenbergAPIAvailableForFullComposability = () => {
return [ BlockPopover ].every(
( api ) => api !== undefined && api !== null
);
};
export const isFullComposabilityFeatureAndAPIAvailable = () => {
+ // @ts-expect-error temp fix
+ if ( window.parent?.window.cys_aiFlow ) {
+ return false;
+ }
+
return (
isPatternToolkitFullComposabilityFeatureFlagEnabled() &&
isGutenbergAPIAvailableForFullComposability()
diff --git a/plugins/woocommerce-admin/client/customize-store/design-without-ai/pages/ApiCallLoader.tsx b/plugins/woocommerce-admin/client/customize-store/design-without-ai/pages/ApiCallLoader.tsx
index 71e85d946c7..fbaa9eeb529 100644
--- a/plugins/woocommerce-admin/client/customize-store/design-without-ai/pages/ApiCallLoader.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/design-without-ai/pages/ApiCallLoader.tsx
@@ -116,10 +116,6 @@ const AssemblerHub = ( { sendEvent }: { sendEvent: SendEventFn } ) => {
if ( event.data?.type === 'INSTALL_FONTS' ) {
sendEvent( { type: 'INSTALL_FONTS' } );
}
-
- if ( event.data?.type === 'INSTALL_PATTERNS' ) {
- sendEvent( { type: 'INSTALL_PATTERNS' } );
- }
} );
}, [ sendEvent ] );
diff --git a/plugins/woocommerce-admin/client/customize-store/design-without-ai/services.ts b/plugins/woocommerce-admin/client/customize-store/design-without-ai/services.ts
index 32aa45fd9a1..48e03d0376b 100644
--- a/plugins/woocommerce-admin/client/customize-store/design-without-ai/services.ts
+++ b/plugins/woocommerce-admin/client/customize-store/design-without-ai/services.ts
@@ -1,10 +1,10 @@
/**
* External dependencies
*/
-import { Sender } from 'xstate';
-import apiFetch from '@wordpress/api-fetch';
-import { resolveSelect, dispatch } from '@wordpress/data';
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
+import apiFetch from '@wordpress/api-fetch';
+import { dispatch, resolveSelect } from '@wordpress/data';
+import { Sender } from 'xstate';
// @ts-expect-error -- No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { mergeBaseAndUserConfigs } from '@wordpress/edit-site/build-module/components/global-styles/global-styles-provider';
@@ -15,24 +15,18 @@ import { store as coreStore } from '@wordpress/core-data';
/**
* Internal dependencies
*/
-import { updateTemplate } from '../data/actions';
-import { HOMEPAGE_TEMPLATES } from '../data/homepageTemplates';
-import { installAndActivateTheme as setTheme } from '../data/service';
-import { THEME_SLUG } from '../data/constants';
-import { FontFace, FontFamily } from '../types/font';
-import {
- FontCollectionResponse,
- installFontFace,
- installFontFamily,
- getFontFamiliesAndFontFaceToInstall,
-} from './fonts';
import { COLOR_PALETTES } from '../assembler-hub/sidebar/global-styles/color-palette-variations/constants';
import {
FONT_PAIRINGS_WHEN_AI_IS_OFFLINE,
FONT_PAIRINGS_WHEN_USER_DID_NOT_ALLOW_TRACKING,
} from '../assembler-hub/sidebar/global-styles/font-pairing-variations/constants';
-import { DesignWithoutAIStateMachineContext, Theme } from './types';
+import { updateTemplate } from '../data/actions';
+import { THEME_SLUG } from '../data/constants';
+import { HOMEPAGE_TEMPLATES } from '../data/homepageTemplates';
+import { installAndActivateTheme as setTheme } from '../data/service';
import { trackEvent } from '../tracking';
+import { DesignWithoutAIStateMachineContext, Theme } from './types';
+import { installFontFamilies as installDefaultFontFamilies } from '../assembler-hub/utils/fonts';
const assembleSite = async () => {
await updateTemplate( {
@@ -128,6 +122,20 @@ const updateGlobalStylesWithDefaultValues = async (
);
};
+const updateShowOnFront = async () => {
+ try {
+ await apiFetch( {
+ path: '/wp/v2/settings',
+ method: 'POST',
+ data: {
+ show_on_front: 'posts',
+ },
+ } );
+ } catch ( error ) {
+ throw error;
+ }
+};
+
const installAndActivateTheme = async (
context: DesignWithoutAIStateMachineContext
) => {
@@ -179,66 +187,7 @@ const installFontFamilies = async () => {
}
try {
- const installedFontFamily = ( await resolveSelect(
- 'core'
- ).getEntityRecords( 'postType', 'wp_font_family', {
- per_page: -1,
- } ) ) as Array< {
- id: number;
- font_faces: Array< number >;
- font_family_settings: FontFamily;
- } >;
-
- const installedFontFamiliesWithFontFaces = await Promise.all(
- installedFontFamily.map( async ( fontFamily ) => {
- const fontFaces = await apiFetch< Array< FontFace > >( {
- path: `/wp/v2/font-families/${ fontFamily.id }/font-faces`,
- method: 'GET',
- } );
-
- return {
- ...fontFamily,
- font_face: fontFaces,
- };
- } )
- );
-
- const fontCollection = await apiFetch< FontCollectionResponse >( {
- path: `/wp/v2/font-collections/google-fonts`,
- method: 'GET',
- } );
-
- const { fontFacesToInstall, fontFamiliesWithFontFacesToInstall } =
- getFontFamiliesAndFontFaceToInstall(
- fontCollection,
- installedFontFamiliesWithFontFaces
- );
-
- const fontFamiliesWithFontFaceToInstallPromises =
- fontFamiliesWithFontFacesToInstall.map( async ( fontFamily ) => {
- const fontFamilyResponse = await installFontFamily(
- fontFamily
- );
- return Promise.all(
- fontFamily.fontFace.map( async ( fontFace, index ) => {
- installFontFace(
- {
- ...fontFace,
- fontFamilyId: fontFamilyResponse.id,
- },
- index
- );
- } )
- );
- } );
-
- const fontFacesToInstallPromises =
- fontFacesToInstall.map( installFontFace );
-
- await Promise.all( [
- ...fontFamiliesWithFontFaceToInstallPromises,
- ...fontFacesToInstallPromises,
- ] );
+ await installDefaultFontFamilies();
} catch ( error ) {
throw error;
}
@@ -281,4 +230,5 @@ export const services = {
installPatterns,
updateGlobalStylesWithDefaultValues,
enableTracking,
+ updateShowOnFront,
};
diff --git a/plugins/woocommerce-admin/client/customize-store/design-without-ai/state-machine.tsx b/plugins/woocommerce-admin/client/customize-store/design-without-ai/state-machine.tsx
index 938bfc528d8..4189fa34bdc 100644
--- a/plugins/woocommerce-admin/client/customize-store/design-without-ai/state-machine.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/design-without-ai/state-machine.tsx
@@ -81,7 +81,6 @@ const installFontFamiliesState = {
export type DesignWithoutAIStateMachineEvents =
| { type: 'EXTERNAL_URL_UPDATE' }
| { type: 'INSTALL_FONTS' }
- | { type: 'INSTALL_PATTERNS' }
| { type: 'NO_AI_FLOW_ERROR'; payload: { hasError: boolean } };
export const designWithNoAiStateMachineDefinition = createMachine(
@@ -103,9 +102,6 @@ export const designWithNoAiStateMachineDefinition = createMachine(
INSTALL_FONTS: {
target: 'installFontFamilies',
},
- INSTALL_PATTERNS: {
- target: 'installPatterns',
- },
},
context: {
startLoadingTime: null,
@@ -213,6 +209,24 @@ export const designWithNoAiStateMachineDefinition = createMachine(
},
type: 'parallel',
states: {
+ updateShowOnFront: {
+ initial: 'pending',
+ states: {
+ pending: {
+ invoke: {
+ src: 'updateShowOnFront',
+ onDone: {
+ target: 'success',
+ },
+ onError: {
+ actions:
+ 'redirectToIntroWithError',
+ },
+ },
+ },
+ success: { type: 'final' },
+ },
+ },
installAndActivateTheme: {
initial: 'pending',
states: {
diff --git a/plugins/woocommerce-admin/client/customize-store/index.tsx b/plugins/woocommerce-admin/client/customize-store/index.tsx
index d315c845ad1..47a4c0c3b9f 100644
--- a/plugins/woocommerce-admin/client/customize-store/index.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/index.tsx
@@ -62,7 +62,6 @@ export type customizeStoreStateMachineEvents =
| { type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION'; payload: { step: string } }
| { type: 'EXTERNAL_URL_UPDATE' }
| { type: 'INSTALL_FONTS' }
- | { type: 'INSTALL_PATTERNS' }
| { type: 'NO_AI_FLOW_ERROR'; payload: { hasError: boolean } }
| { type: 'IS_FONT_LIBRARY_AVAILABLE'; payload: boolean };
@@ -242,9 +241,6 @@ export const customizeStoreStateMachineDefinition = createMachine( {
INSTALL_FONTS: {
target: 'designWithoutAi.installFonts',
},
- INSTALL_PATTERNS: {
- target: 'designWithoutAi.installPatterns',
- },
},
states: {
setFlags: {
diff --git a/plugins/woocommerce-admin/client/customize-store/intro/index.tsx b/plugins/woocommerce-admin/client/customize-store/intro/index.tsx
index 7f31b028a68..73d7a9609ec 100644
--- a/plugins/woocommerce-admin/client/customize-store/intro/index.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/intro/index.tsx
@@ -25,6 +25,7 @@ import {
DesignChangeWarningModal,
StartNewDesignWarningModal,
StartOverWarningModal,
+ ThemeSwitchWarningModal,
} from './warning-modals';
import { useNetworkStatus } from '~/utils/react-hooks/use-network-status';
import './intro.scss';
@@ -37,6 +38,8 @@ import {
ExistingThemeBanner,
NoAIBanner,
ExistingNoAiThemeBanner,
+ ClassicThemeBanner,
+ NonDefaultBlockThemeBanner,
} from './intro-banners';
import welcomeTourImg from '../assets/images/design-your-own.svg';
import professionalThemeImg from '../assets/images/professional-theme.svg';
@@ -44,6 +47,7 @@ import { navigateOrParent } from '~/customize-store/utils';
import { RecommendThemesAPIResponse } from '~/customize-store/types';
import { customizeStoreStateMachineEvents } from '~/customize-store';
import { trackEvent } from '~/customize-store/tracking';
+import { isNoAIFlow as isNoAiFlowGuard } from '../guards';
export type events =
| { type: 'DESIGN_WITH_AI' }
@@ -67,6 +71,8 @@ const BANNER_COMPONENTS = {
'existing-theme': ExistingThemeBanner,
[ FlowType.noAI ]: NoAIBanner,
'existing-no-ai-theme': ExistingNoAiThemeBanner,
+ 'classic-theme': ClassicThemeBanner,
+ 'non-default-block-theme': NonDefaultBlockThemeBanner,
default: DefaultBanner,
};
@@ -143,11 +149,17 @@ const ThemeCards = ( {
const CustomizedThemeBanners = ( {
isBlockTheme,
+ isDefaultTheme,
+ isNoAiFlow,
sendEvent,
}: {
isBlockTheme: boolean | undefined;
+ isDefaultTheme: boolean | undefined;
+ isNoAiFlow: boolean;
sendEvent: Sender< customizeStoreStateMachineEvents >;
} ) => {
+ const [ isModalOpen, setIsModalOpen ] = useState( false );
+
return (
<>
@@ -177,7 +189,7 @@ const CustomizedThemeBanners = ( {
: 'classic',
}
);
- if ( isBlockTheme ) {
+ if ( isDefaultTheme && isNoAiFlow ) {
navigateOrParent(
window,
getNewPath(
@@ -187,10 +199,7 @@ const CustomizedThemeBanners = ( {
)
);
} else {
- navigateOrParent(
- window,
- 'customize.php?return=/wp-admin/themes.php'
- );
+ setIsModalOpen( true );
}
} }
>
@@ -232,6 +241,19 @@ const CustomizedThemeBanners = ( {
+ { isModalOpen && (
+
+ sendEvent( {
+ type: isNoAiFlow
+ ? 'DESIGN_WITHOUT_AI'
+ : 'DESIGN_WITH_AI',
+ } )
+ }
+ />
+ ) }
>
);
};
@@ -281,14 +303,16 @@ export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => {
case isJetpackOffline as boolean:
bannerStatus = 'jetpack-offline';
break;
- case context.flowType === FlowType.noAI &&
- ! customizeStoreTaskCompleted:
- bannerStatus = FlowType.noAI;
+ case context.flowType === FlowType.noAI && ! isBlockTheme:
+ bannerStatus = 'classic-theme';
break;
case context.flowType === FlowType.noAI &&
- customizeStoreTaskCompleted &&
isBlockTheme &&
! isDefaultTheme:
+ bannerStatus = 'non-default-block-theme';
+ break;
+ case context.flowType === FlowType.noAI &&
+ ! customizeStoreTaskCompleted:
bannerStatus = FlowType.noAI;
break;
case context.flowType === FlowType.noAI && customizeStoreTaskCompleted:
@@ -407,17 +431,18 @@ export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => {
sendEvent={ sendEvent }
/>
- { customizeStoreTaskCompleted &&
- ( isDefaultTheme || ! isBlockTheme ) ? (
-
- ) : (
+ { isDefaultTheme && ! customizeStoreTaskCompleted ? (
+ ) : (
+
) }
diff --git a/plugins/woocommerce-admin/client/customize-store/intro/intro-banners.tsx b/plugins/woocommerce-admin/client/customize-store/intro/intro-banners.tsx
index 35a8024cc9f..c93a141b298 100644
--- a/plugins/woocommerce-admin/client/customize-store/intro/intro-banners.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/intro/intro-banners.tsx
@@ -7,8 +7,6 @@ import { Button } from '@wordpress/components';
import { getNewPath } from '@woocommerce/navigation';
import interpolateComponents from '@automattic/interpolate-components';
import { Link } from '@woocommerce/components';
-import { useState } from '@wordpress/element';
-import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
@@ -17,7 +15,6 @@ import { Intro } from '.';
import { IntroSiteIframe } from './intro-site-iframe';
import { getAdminSetting } from '~/utils/admin-settings';
import { navigateOrParent } from '../utils';
-import { ThemeSwitchWarningModal } from '~/customize-store/intro/warning-modals';
import { trackEvent } from '../tracking';
export const BaseIntroBanner = ( {
@@ -223,20 +220,6 @@ export const NoAIBanner = ( {
}: {
redirectToCYSFlow: () => void;
} ) => {
- const [ isModalOpen, setIsModalOpen ] = useState( false );
- interface Theme {
- is_block_theme?: boolean;
- stylesheet?: string;
- }
-
- const currentTheme = useSelect( ( select ) => {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- return select( 'core' ).getCurrentTheme() as Theme;
- }, [] );
-
- const isDefaultTheme = currentTheme?.stylesheet === 'twentytwentyfour';
-
return (
<>
{
- if ( ! isDefaultTheme ) {
- setIsModalOpen( true );
- } else {
- redirectToCYSFlow();
- }
+ redirectToCYSFlow();
} }
showAIDisclaimer={ false }
/>
- { isModalOpen && (
-
- ) }
>
);
};
@@ -323,48 +296,82 @@ export const ExistingAiThemeBanner = ( {
export const ExistingNoAiThemeBanner = () => {
const siteUrl = getAdminSetting( 'siteUrl' ) + '?cys-hide-admin-bar=1';
- interface Theme {
- is_block_theme?: boolean;
- }
-
- const currentTheme = useSelect( ( select ) => {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- return select( 'core' ).getCurrentTheme() as Theme;
- }, [] );
-
- const isBlockTheme = currentTheme?.is_block_theme;
-
return (
{
trackEvent( 'customize_your_store_intro_customize_click', {
- theme_type: isBlockTheme ? 'block' : 'classic',
+ theme_type: 'block',
} );
- if ( isBlockTheme ) {
- navigateOrParent(
- window,
- getNewPath(
- { customizing: true },
- '/customize-store/assembler-hub',
- {}
- )
- );
- } else {
- navigateOrParent(
- window,
- 'customize.php?return=/wp-admin/themes.php'
- );
- }
+ navigateOrParent(
+ window,
+ getNewPath(
+ { customizing: true },
+ '/customize-store/assembler-hub',
+ {}
+ )
+ );
} }
- bannerButtonText={ __( 'Customize your theme', 'woocommerce' ) }
+ bannerButtonText={ __( 'Customize your store', 'woocommerce' ) }
+ showAIDisclaimer={ false }
+ previewBanner={ }
+ >
+ );
+};
+
+export const ClassicThemeBanner = () => {
+ const siteUrl = getAdminSetting( 'siteUrl' ) + '?cys-hide-admin-bar=1';
+
+ return (
+ {
+ trackEvent( 'customize_your_store_intro_customize_click', {
+ theme_type: 'classic',
+ } );
+ navigateOrParent(
+ window,
+ 'customize.php?return=/wp-admin/themes.php'
+ );
+ } }
+ bannerButtonText={ __( 'Go to the Customizer', 'woocommerce' ) }
+ showAIDisclaimer={ false }
+ previewBanner={ }
+ >
+ );
+};
+
+export const NonDefaultBlockThemeBanner = () => {
+ const siteUrl = getAdminSetting( 'siteUrl' ) + '?cys-hide-admin-bar=1';
+
+ return (
+ {
+ trackEvent( 'customize_your_store_intro_customize_click', {
+ theme_type: 'block',
+ } );
+ navigateOrParent( window, 'site-editor.php' );
+ } }
+ bannerButtonText={ __( 'Go to the Editor', 'woocommerce' ) }
showAIDisclaimer={ false }
previewBanner={ }
>
diff --git a/plugins/woocommerce-admin/client/customize-store/intro/intro.scss b/plugins/woocommerce-admin/client/customize-store/intro/intro.scss
index e8e8fd9466b..cd47827cdea 100644
--- a/plugins/woocommerce-admin/client/customize-store/intro/intro.scss
+++ b/plugins/woocommerce-admin/client/customize-store/intro/intro.scss
@@ -224,10 +224,12 @@
text-align: center;
padding: 36px 51px;
margin-top: 0;
- width: 500px;
+ width: 390px;
+ max-height: 320px;
img {
width: 100%;
+ height: 170px;
margin-bottom: 20px;
}
diff --git a/plugins/woocommerce-admin/client/customize-store/intro/services.ts b/plugins/woocommerce-admin/client/customize-store/intro/services.ts
index e4634fc8508..73ecd0f0092 100644
--- a/plugins/woocommerce-admin/client/customize-store/intro/services.ts
+++ b/plugins/woocommerce-admin/client/customize-store/intro/services.ts
@@ -161,6 +161,9 @@ export const setFlags = async () => {
online: isAiOnline ? 'yes' : 'no',
} );
+ // @ts-expect-error temp workaround;
+ window.cys_aiFlow = true;
+
return isAiOnline ? FlowType.AIOnline : FlowType.AIOffline;
} catch ( e ) {
// @ts-expect-error temp workaround;
diff --git a/plugins/woocommerce-admin/client/customize-store/intro/tests/intro-banner.test.tsx b/plugins/woocommerce-admin/client/customize-store/intro/tests/intro-banner.test.tsx
index 8f496dc8871..9487a743c83 100644
--- a/plugins/woocommerce-admin/client/customize-store/intro/tests/intro-banner.test.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/intro/tests/intro-banner.test.tsx
@@ -19,6 +19,16 @@ jest.mock( '../../assembler-hub/site-hub', () => ( {
jest.mock( '~/utils/react-hooks/use-network-status', () => ( {
useNetworkStatus: jest.fn(),
} ) );
+
+jest.mock( '@wordpress/data', () => {
+ const originalModule = jest.requireActual( '@wordpress/data' );
+ return {
+ ...originalModule,
+ useSelect: jest.fn( () => ( {
+ is_block_theme: true,
+ } ) ),
+ };
+} );
describe( 'Intro Banners', () => {
it( 'should display NetworkOfflineBanner when network is offline', () => {
( useNetworkStatus as jest.Mock ).mockImplementation( () => true );
diff --git a/plugins/woocommerce-admin/client/customize-store/intro/tests/intro-modal.test.tsx b/plugins/woocommerce-admin/client/customize-store/intro/tests/intro-modal.test.tsx
index a53d40069c3..d60c4689445 100644
--- a/plugins/woocommerce-admin/client/customize-store/intro/tests/intro-modal.test.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/intro/tests/intro-modal.test.tsx
@@ -17,6 +17,16 @@ jest.mock( '../../assembler-hub/site-hub', () => ( {
jest.mock( '~/utils/react-hooks/use-network-status', () => ( {
useNetworkStatus: jest.fn(),
} ) );
+
+jest.mock( '@wordpress/data', () => {
+ const originalModule = jest.requireActual( '@wordpress/data' );
+ return {
+ ...originalModule,
+ useSelect: jest.fn( () => ( {
+ is_block_theme: true,
+ } ) ),
+ };
+} );
describe( 'Intro Modals', () => {
it( 'should display DesignChangeWarningModal when activeThemeHasMods and button is clicked', async () => {
const sendEventMock = jest.fn();
diff --git a/plugins/woocommerce-admin/client/customize-store/intro/warning-modals.tsx b/plugins/woocommerce-admin/client/customize-store/intro/warning-modals.tsx
index f1af598caf0..9e8f242f8b9 100644
--- a/plugins/woocommerce-admin/client/customize-store/intro/warning-modals.tsx
+++ b/plugins/woocommerce-admin/client/customize-store/intro/warning-modals.tsx
@@ -196,9 +196,11 @@ export const StartOverWarningModal = ( {
};
export const ThemeSwitchWarningModal = ( {
+ isNoAiFlow = true,
setIsModalOpen,
redirectToCYSFlow,
}: {
+ isNoAiFlow?: boolean;
setIsModalOpen: ( arg0: boolean ) => void;
redirectToCYSFlow: () => void;
} ) => {
@@ -215,10 +217,31 @@ export const ThemeSwitchWarningModal = ( {
shouldCloseOnClickOutside={ false }
>
- { __(
- 'Your active theme will be changed and you could lose any changes you’ve made to it.',
- 'woocommerce'
- ) }
+ { isNoAiFlow
+ ? __(
+ 'Your active theme will be changed and you could lose any changes you’ve made to it.',
+ 'woocommerce'
+ )
+ : createInterpolateElement(
+ __(
+ "The Store Designer will create a new store design for you, and you'll lose any changes you've made to your active theme. If you'd prefer to continue editing your theme, you can do so via the Editor .",
+ 'woocommerce'
+ ),
+ {
+ EditorLink: (
+ {
+ window.open(
+ `${ ADMIN_URL }site-editor.php`,
+ '_blank'
+ );
+ return false;
+ } }
+ href=""
+ />
+ ),
+ }
+ ) }
{ isEntrepreneurFlow()
? __(
- "Congratuations! You've successfully designed your store. Now you can go back to the Home screen to complete your store setup and start selling.",
+ "Congratulations! You've successfully designed your store. Now you can go back to the Home screen to complete your store setup and start selling.",
'woocommerce'
)
: __(
diff --git a/plugins/woocommerce-admin/client/index.js b/plugins/woocommerce-admin/client/index.js
index e7e3b0af604..7b64b67829f 100644
--- a/plugins/woocommerce-admin/client/index.js
+++ b/plugins/woocommerce-admin/client/index.js
@@ -2,7 +2,7 @@
* External dependencies
*/
import '@wordpress/notices';
-import { render } from '@wordpress/element';
+import { render, createRoot } from '@wordpress/element';
import { CustomerEffortScoreTracksContainer } from '@woocommerce/customer-effort-score';
import {
withCurrentUserHydration,
@@ -22,6 +22,11 @@ import { possiblyRenderSettingsSlots } from './settings/settings-slots';
import { registerTaxSettingsConflictErrorFill } from './settings/conflict-error-slotfill';
import { registerPaymentsSettingsBannerFill } from './payments/payments-settings-banner-slotfill';
import { registerSiteVisibilitySlotFill } from './launch-your-store';
+import {
+ SettingsPaymentsMainWrapper,
+ SettingsPaymentsOfflineWrapper,
+ SettingsPaymentsWooCommercePaymentsWrapper,
+} from './settings-payments';
import { ErrorBoundary } from './error-boundary';
const appRoot = document.getElementById( 'root' );
@@ -119,3 +124,49 @@ if (
);
} )();
}
+
+// Render the payment settings components only if
+// the feature flag is enabled.
+if (
+ window.wcAdminFeatures &&
+ window.wcAdminFeatures[ 'reactify-classic-payments-settings' ] === true
+) {
+ ( function () {
+ const paymentsMainRoot = document.getElementById(
+ 'experimental_wc_settings_payments_main'
+ );
+ const paymentsOfflineRoot = document.getElementById(
+ 'experimental_wc_settings_payments_offline'
+ );
+ const paymentsWooCommercePaymentsRoot = document.getElementById(
+ 'experimental_wc_settings_payments_woocommerce_payments'
+ );
+
+ if ( paymentsMainRoot ) {
+ createRoot(
+ paymentsMainRoot.insertBefore(
+ document.createElement( 'div' ),
+ null
+ )
+ ).render( );
+ }
+
+ if ( paymentsOfflineRoot ) {
+ createRoot(
+ paymentsOfflineRoot.insertBefore(
+ document.createElement( 'div' ),
+ null
+ )
+ ).render( );
+ }
+
+ if ( paymentsWooCommercePaymentsRoot ) {
+ createRoot(
+ paymentsWooCommercePaymentsRoot.insertBefore(
+ document.createElement( 'div' ),
+ null
+ )
+ ).render( );
+ }
+ } )();
+}
diff --git a/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/index.tsx b/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/index.tsx
index 0c001c159b9..c0c08602013 100644
--- a/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/index.tsx
+++ b/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/index.tsx
@@ -45,6 +45,7 @@ export type events =
};
import { WhatsNext } from './WhatsNext';
import { LysSurvey } from './Survey';
+import { useFullScreen } from '~/utils';
export const LaunchYourStoreSuccess = ( {
context: {
@@ -75,6 +76,8 @@ export const LaunchYourStoreSuccess = ( {
}
);
+ useFullScreen( [ 'woocommerce-launch-your-store-success' ] );
+
return (
diff --git a/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/style.scss b/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/style.scss
index 9450fe77f31..03a6776c3a0 100644
--- a/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/style.scss
+++ b/plugins/woocommerce-admin/client/launch-your-store/hub/main-content/pages/launch-store-success/style.scss
@@ -18,7 +18,8 @@
display: flex;
flex-direction: column;
width: 100vw;
- height: 100vh;
+ min-height: 100vh;
+ max-width: 100vw;
.woocommerce-launch-store__congrats-header-container {
min-height: 64px;
diff --git a/plugins/woocommerce-admin/client/launch-your-store/hub/styles.scss b/plugins/woocommerce-admin/client/launch-your-store/hub/styles.scss
index 069c333dab5..5425cd95cd6 100644
--- a/plugins/woocommerce-admin/client/launch-your-store/hub/styles.scss
+++ b/plugins/woocommerce-admin/client/launch-your-store/hub/styles.scss
@@ -2,6 +2,11 @@
min-width: 600px;
overflow: auto;
+ &.woocommerce-launch-your-store-success {
+ overflow-x: clip;
+ min-width: unset;
+ }
+
.woocommerce-layout__primary {
margin: 0;
padding: 0;
@@ -14,6 +19,7 @@
display: flex;
flex-direction: row;
background-color: #fcfcfc;
+ min-height: 100vh;
.customize-your-store-edit-site-resizable-frame__inner-content {
height: 100%;
diff --git a/plugins/woocommerce-admin/client/layout/store-alerts/index.js b/plugins/woocommerce-admin/client/layout/store-alerts/index.js
index c299e2ecaea..e8f94361663 100644
--- a/plugins/woocommerce-admin/client/layout/store-alerts/index.js
+++ b/plugins/woocommerce-admin/client/layout/store-alerts/index.js
@@ -24,7 +24,6 @@ import {
useUserPreferences,
} from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
-import { Text } from '@woocommerce/experimental';
import {
navigateTo,
parseAdminUrl,
@@ -107,12 +106,12 @@ export const StoreAlerts = () => {
}
function renderActions( alert ) {
- const actions = alert.actions.map( ( action ) => {
+ const actions = alert.actions.map( ( action, idx ) => {
+ const variant = idx === 0 ? 'secondary' : 'tertiary';
return (
{
const url = event.currentTarget.getAttribute( 'href' );
@@ -315,33 +314,15 @@ export const StoreAlerts = () => {
return (
- onDismiss( alert ) }
+
-
-
-
-
+
{ alert.title }
-
+
{ numberOfAlerts > 1 && (
-
-
-
{
},
} ) }
+
+
+
{
) }
+ onDismiss( alert ) }
+ >
+
+
{
} );
const { container } = render(
);
- expect( container.querySelector( 'h2' ).textContent ).toBe(
- 'Alert title 1'
- );
+ expect(
+ container.querySelector( '.woocommerce-store-alerts__title' )
+ .textContent
+ ).toBe( 'Alert title 1' );
expect(
container.querySelector( '.woocommerce-store-alerts__message' )
.textContent
@@ -171,24 +172,27 @@ describe( 'StoreAlerts', () => {
);
- expect( container.querySelector( 'h2' ).textContent ).toBe(
- 'Alert title 1'
- );
+ expect(
+ container.querySelector( '.woocommerce-store-alerts__title' )
+ .textContent
+ ).toBe( 'Alert title 1' );
fireEvent.click( getByLabelText( 'Next Alert' ) );
rerender(
);
- expect( container.querySelector( 'h2' ).textContent ).toBe(
- 'Alert title 2'
- );
+ expect(
+ container.querySelector( '.woocommerce-store-alerts__title' )
+ .textContent
+ ).toBe( 'Alert title 2' );
fireEvent.click( getByLabelText( 'Previous Alert' ) );
rerender(
);
- expect( container.querySelector( 'h2' ).textContent ).toBe(
- 'Alert title 1'
- );
+ expect(
+ container.querySelector( '.woocommerce-store-alerts__title' )
+ .textContent
+ ).toBe( 'Alert title 1' );
} );
} );
diff --git a/plugins/woocommerce-admin/client/lib/async-requests/index.js b/plugins/woocommerce-admin/client/lib/async-requests/index.js
index b2859557b5c..7e5e997e88d 100644
--- a/plugins/woocommerce-admin/client/lib/async-requests/index.js
+++ b/plugins/woocommerce-admin/client/lib/async-requests/index.js
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+import { __, sprintf } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import { identity } from 'lodash';
@@ -106,7 +107,19 @@ export function getVariationName( { attributes, name } ) {
}
const attributeList = ( attributes || [] )
- .map( ( { option } ) => option )
+ .map( ( { name: attributeName, option } ) => {
+ if ( ! option ) {
+ attributeName =
+ attributeName.charAt( 0 ).toUpperCase() +
+ attributeName.slice( 1 );
+ option = sprintf(
+ // translators: %s: the attribute name.
+ __( 'Any %s', 'woocommerce' ),
+ attributeName
+ );
+ }
+ return option;
+ } )
.join( ', ' );
return attributeList ? name + separator + attributeList : name;
diff --git a/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.test.tsx b/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.test.tsx
index 4351e7f9d28..486631e197a 100644
--- a/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.test.tsx
+++ b/plugins/woocommerce-admin/client/marketing/components/CreateNewCampaignModal/CreateNewCampaignModal.test.tsx
@@ -35,7 +35,7 @@ const google = {
'Boost your product listings with a campaign that is automatically optimized to meet your goals.',
createUrl:
'https://wc1.test/wp-admin/admin.php?page=wc-admin&path=/google/dashboard&subpath=/campaigns/create',
- channelName: 'Google Listings and Ads',
+ channelName: 'Google for WooCommerce',
channelSlug: 'google-listings-and-ads',
};
diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.test.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.test.tsx
index cfa9088b95b..e281b8306e8 100644
--- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.test.tsx
+++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/Campaigns/Campaigns.test.tsx
@@ -36,7 +36,7 @@ const createTestCampaign = ( programId: string ) => {
cost: `USD 30`,
manageUrl: `https://wc1.test/wp-admin/admin.php?page=wc-admin&path=/google/dashboard&subpath=/campaigns/edit&programId=${ programId }`,
icon: 'https://woocommerce.com/wp-content/uploads/2021/06/woo-GoogleListingsAds-jworee.png',
- channelName: 'Google Listings and Ads',
+ channelName: 'Google for WooCommerce',
channelSlug: 'google-listings-and-ads',
};
};
@@ -198,7 +198,7 @@ describe( 'Campaigns component', () => {
'Boost your product listings with a campaign that is automatically optimized to meet your goals.',
channel: {
slug: 'google-listings-and-ads',
- name: 'Google Listings & Ads',
+ name: 'Google for WooCommerce',
},
create_url:
'https://wc1.test/wp-admin/admin.php?page=wc-admin&path=/google/dashboard&subpath=/campaigns/create',
diff --git a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.test.tsx b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.test.tsx
index f173ea50638..a205fccb4a2 100644
--- a/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.test.tsx
+++ b/plugins/woocommerce-admin/client/marketing/overview-multichannel/DiscoverTools/DiscoverTools.test.tsx
@@ -66,7 +66,7 @@ describe( 'DiscoverTools component', () => {
isLoading: false,
data: [
{
- title: 'Google Listings and Ads',
+ title: 'Google for WooCommerce',
description:
'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.',
url: 'https://woocommerce.com/products/google-listings-and-ads/?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons',
@@ -95,7 +95,7 @@ describe( 'DiscoverTools component', () => {
// Assert that we have the "Sales channels" tab, the plugin name, the "Built by WooCommerce" pill, and the "Install extension" button.
expect( screen.getByText( 'Sales channels' ) ).toBeInTheDocument();
expect(
- screen.getByText( 'Google Listings and Ads' )
+ screen.getByText( 'Google for WooCommerce' )
).toBeInTheDocument();
expect(
screen.getByText( 'Built by WooCommerce' )
diff --git a/plugins/woocommerce-admin/client/marketplace/marketplace.scss b/plugins/woocommerce-admin/client/marketplace/marketplace.scss
index 9e50372b7ef..5a0813ae16d 100644
--- a/plugins/woocommerce-admin/client/marketplace/marketplace.scss
+++ b/plugins/woocommerce-admin/client/marketplace/marketplace.scss
@@ -22,14 +22,13 @@
/* On marketplace pages, reposition store alerts so they don't collide with other components */
.woocommerce-store-alerts {
- margin-left: 16px;
+ margin-left: 24px;
margin-right: 16px;
margin-top: 16px;
- @media (min-width: $breakpoint-medium) {
- margin-left: 32px;
- margin-right: 32px;
- margin-top: 32px;
+ @include breakpoint("<782px") {
+ margin-left: 16px;
+ width: 100%;
}
}
}
diff --git a/plugins/woocommerce-admin/client/payments-welcome/banner.tsx b/plugins/woocommerce-admin/client/payments-welcome/banner.tsx
index e4b00250c5b..33273b0ecad 100644
--- a/plugins/woocommerce-admin/client/payments-welcome/banner.tsx
+++ b/plugins/woocommerce-admin/client/payments-welcome/banner.tsx
@@ -3,6 +3,7 @@
*/
import { Card, CardBody, Button, CardDivider } from '@wordpress/components';
import { useState } from '@wordpress/element';
+import { WooPaymentMethodsLogos } from '@woocommerce/onboarding';
/**
* Internal dependencies
@@ -11,7 +12,6 @@ import { getAdminSetting } from '~/utils/admin-settings';
import sanitizeHTML from '~/lib/sanitize-html';
import WooPaymentsLogo from './woopayments.svg';
import ExitSurveyModal from './exit-survey-modal';
-import PaymentMethods from './payment-methods';
import strings from './strings';
interface Props {
@@ -76,7 +76,10 @@ const Banner: React.FC< Props > = ( { isSubmitted, handleSetup } ) => {
{ strings.paymentOptions }
-
+
{ isExitSurveyModalOpen && (
{
- const wccomSettings = getAdminSetting( 'wccomHelper', false );
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- { wccomSettings && wccomSettings.storeCountry === 'GB' ? (
-
- ) : (
-
- ) }
-
{ strings.andMore }
-
- );
-};
-
-export default PaymentMethods;
diff --git a/plugins/woocommerce-admin/client/payments/payment-recommendations-wrapper.tsx b/plugins/woocommerce-admin/client/payments/payment-recommendations-wrapper.tsx
index 3791d48d33c..1a57546b462 100644
--- a/plugins/woocommerce-admin/client/payments/payment-recommendations-wrapper.tsx
+++ b/plugins/woocommerce-admin/client/payments/payment-recommendations-wrapper.tsx
@@ -22,6 +22,14 @@ export const PaymentRecommendations: React.FC< EmbeddedBodyProps > = ( {
section,
} ) => {
if ( page === 'wc-settings' && tab === 'checkout' && ! section ) {
+ if (
+ window?.wcAdminFeatures?.[
+ 'reactify-classic-payments-settings'
+ ] === true
+ ) {
+ return null;
+ }
+
return (
diff --git a/plugins/woocommerce-admin/client/products/product-page.tsx b/plugins/woocommerce-admin/client/products/product-page.tsx
index fbe401fd4b5..5586b098057 100644
--- a/plugins/woocommerce-admin/client/products/product-page.tsx
+++ b/plugins/woocommerce-admin/client/products/product-page.tsx
@@ -42,8 +42,22 @@ const ProductMVPFeedbackModalContainer = lazy( () =>
export default function ProductPage() {
const { productId: productIdSearchParam } = useParams();
+ /**
+ * Only register blocks and unregister them when the product page is being rendered or unmounted.
+ * Note: Dependency array should stay empty.
+ */
useEffect( () => {
document.body.classList.add( 'is-product-editor' );
+
+ const unregisterBlocks = initBlocks();
+
+ return () => {
+ document.body.classList.remove( 'is-product-editor' );
+ unregisterBlocks();
+ };
+ }, [] );
+
+ useEffect( () => {
registerPlugin( 'wc-admin-product-editor', {
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
scope: 'woocommerce-product-block-editor',
@@ -89,12 +103,8 @@ export default function ProductPage() {
},
} );
- const unregisterBlocks = initBlocks();
-
return () => {
- document.body.classList.remove( 'is-product-editor' );
unregisterPlugin( 'wc-admin-product-editor' );
- unregisterBlocks();
};
}, [ productIdSearchParam ] );
diff --git a/plugins/woocommerce-admin/client/settings-payments/index.tsx b/plugins/woocommerce-admin/client/settings-payments/index.tsx
new file mode 100644
index 00000000000..90ef3dceeaa
--- /dev/null
+++ b/plugins/woocommerce-admin/client/settings-payments/index.tsx
@@ -0,0 +1,51 @@
+/**
+ * External dependencies
+ */
+import { lazy, Suspense } from '@wordpress/element';
+
+const SettingsPaymentsMainChunk = lazy(
+ () =>
+ import(
+ /* webpackChunkName: "settings-payments-main" */ './settings-payments-main'
+ )
+);
+
+const SettingsPaymentsOfflineChunk = lazy(
+ () =>
+ import(
+ /* webpackChunkName: "settings-payments-offline" */ './settings-payments-offline'
+ )
+);
+
+const SettingsPaymentsWooCommercePaymentsChunk = lazy(
+ () =>
+ import(
+ /* webpackChunkName: "settings-payments-woocommerce-payments" */ './settings-payments-woocommerce-payments'
+ )
+);
+
+export const SettingsPaymentsMainWrapper: React.FC = () => {
+ return (
+ Loading main settings... }>
+
+
+ );
+};
+
+export const SettingsPaymentsOfflineWrapper: React.FC = () => {
+ return (
+ Loading offline settings... }>
+
+
+ );
+};
+
+export const SettingsPaymentsWooCommercePaymentsWrapper: React.FC = () => {
+ return (
+ Loading WooCommerce Payments settings... }
+ >
+
+
+ );
+};
diff --git a/plugins/woocommerce-admin/client/settings-payments/settings-payments-main.scss b/plugins/woocommerce-admin/client/settings-payments/settings-payments-main.scss
new file mode 100644
index 00000000000..a63d140104c
--- /dev/null
+++ b/plugins/woocommerce-admin/client/settings-payments/settings-payments-main.scss
@@ -0,0 +1,9 @@
+.settings-payments-main__container {
+ h1 {
+ color: #fff;
+ }
+ background: #000;
+ text-align: center;
+ padding: 50px 0;
+ width: 100%;
+}
diff --git a/plugins/woocommerce-admin/client/settings-payments/settings-payments-main.tsx b/plugins/woocommerce-admin/client/settings-payments/settings-payments-main.tsx
new file mode 100644
index 00000000000..c2671e205bf
--- /dev/null
+++ b/plugins/woocommerce-admin/client/settings-payments/settings-payments-main.tsx
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+import '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import './settings-payments-main.scss';
+
+export const SettingsPaymentsMain: React.FC = () => {
+ return (
+
+
Main payments screen
+
+ );
+};
+
+export default SettingsPaymentsMain;
diff --git a/plugins/woocommerce-admin/client/settings-payments/settings-payments-offline.scss b/plugins/woocommerce-admin/client/settings-payments/settings-payments-offline.scss
new file mode 100644
index 00000000000..9a475e038c2
--- /dev/null
+++ b/plugins/woocommerce-admin/client/settings-payments/settings-payments-offline.scss
@@ -0,0 +1,9 @@
+.settings-payments-offline__container {
+ h1 {
+ color: #fff;
+ }
+ background: #333;
+ text-align: center;
+ padding: 50px 0;
+ width: 100%;
+}
diff --git a/plugins/woocommerce-admin/client/settings-payments/settings-payments-offline.tsx b/plugins/woocommerce-admin/client/settings-payments/settings-payments-offline.tsx
new file mode 100644
index 00000000000..2cb35729b41
--- /dev/null
+++ b/plugins/woocommerce-admin/client/settings-payments/settings-payments-offline.tsx
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+import '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import './settings-payments-offline.scss';
+
+export const SettingsPaymentsOffline: React.FC = () => {
+ return (
+
+
Offline payments screen
+
+ );
+};
+
+export default SettingsPaymentsOffline;
diff --git a/plugins/woocommerce-admin/client/settings-payments/settings-payments-woocommerce-payments.scss b/plugins/woocommerce-admin/client/settings-payments/settings-payments-woocommerce-payments.scss
new file mode 100644
index 00000000000..050c50f96ad
--- /dev/null
+++ b/plugins/woocommerce-admin/client/settings-payments/settings-payments-woocommerce-payments.scss
@@ -0,0 +1,9 @@
+.settings-payments-woocommerce-payments__container {
+ h1 {
+ color: #fff;
+ }
+ background: #333;
+ text-align: center;
+ padding: 50px 0;
+ width: 100%;
+}
diff --git a/plugins/woocommerce-admin/client/settings-payments/settings-payments-woocommerce-payments.tsx b/plugins/woocommerce-admin/client/settings-payments/settings-payments-woocommerce-payments.tsx
new file mode 100644
index 00000000000..ba43b7cf50c
--- /dev/null
+++ b/plugins/woocommerce-admin/client/settings-payments/settings-payments-woocommerce-payments.tsx
@@ -0,0 +1,14 @@
+/**
+ * Internal dependencies
+ */
+import './settings-payments-woocommerce-payments.scss';
+
+export const SettingsPaymentsWooCommercePayments: React.FC = () => {
+ return (
+
+
WooCommerce Payments Settings
+
+ );
+};
+
+export default SettingsPaymentsWooCommercePayments;
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/components/WCPayBNPL/Suggestion.js b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/components/WCPayBNPL/Suggestion.js
new file mode 100644
index 00000000000..bfdcc450b37
--- /dev/null
+++ b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/components/WCPayBNPL/Suggestion.js
@@ -0,0 +1,71 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Button, Card, CardBody, CardHeader } from '@wordpress/components';
+import { recordEvent } from '@woocommerce/tracks';
+import { addQueryArgs } from '@wordpress/url';
+
+/**
+ * Internal dependencies
+ */
+import './suggestion.scss';
+
+const recordTrack = () => {
+ recordEvent( 'tasklist_payments_wcpay_bnpl_click' );
+};
+
+export const Suggestion = ( { paymentGateway } ) => {
+ const { id, title, content, settingsUrl, image } = paymentGateway;
+
+ // If there is no settingsUrl, bail.
+ if ( ! settingsUrl ) {
+ return null;
+ }
+
+ const customizedSettingsUrl = addQueryArgs( settingsUrl, {
+ from: 'WCADMIN_PAYMENT_TASK',
+ } );
+
+ return (
+
+
+
+ { title }
+
+
+
+
+ { content }
+
+
+ { __( 'Get started', 'woocommerce' ) }
+
+
+
+
+ { image && (
+
+ ) }
+
+ );
+};
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/components/WCPayBNPL/index.js b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/components/WCPayBNPL/index.js
new file mode 100644
index 00000000000..d7319fc5114
--- /dev/null
+++ b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/components/WCPayBNPL/index.js
@@ -0,0 +1 @@
+export { Suggestion as WCPayBNPLSuggestion } from './Suggestion';
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/components/WCPayBNPL/suggestion.scss b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/components/WCPayBNPL/suggestion.scss
new file mode 100644
index 00000000000..a264284bbfd
--- /dev/null
+++ b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/components/WCPayBNPL/suggestion.scss
@@ -0,0 +1,51 @@
+.woocommerce-wcpay-bnpl-suggestion {
+
+ .woocommerce-wcpay-bnpl-suggestion__body {
+ padding: 0;
+ justify-content: space-between;
+ }
+
+ .woocommerce-wcpay-bnpl-suggestion__contents-container {
+ padding: calc($gap-larger - $gap-smallest) $gap-large;
+ position: relative;
+ flex: 1;
+ overflow: hidden;
+ width: 100%;
+ }
+
+ .svg-background {
+ @include breakpoint("<600px") {
+ display: none;
+ }
+
+ position: absolute;
+ z-index: 0;
+ right: 0;
+ top: 0;
+ width: auto;
+ height: 100%;
+ }
+
+ .woocommerce-wcpay-bnpl-suggestion__contents {
+ @include breakpoint( "<600px") {
+ max-width: 100%;
+ }
+
+ max-width: 71%;
+
+ p {
+ color: $studio-gray-50;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ }
+
+ p:first-of-type {
+ margin-top: $gap-smaller;
+ }
+
+ // This is required in order to have svg image as background.
+ position: relative;
+ z-index: 1;
+ }
+}
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/index.js b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/index.js
index d552283db4b..1b6752f98b1 100644
--- a/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/index.js
+++ b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/index.js
@@ -23,6 +23,7 @@ import ExternalIcon from 'gridicons/dist/external';
import { List, Placeholder as ListPlaceholder } from './components/List';
import { Setup, Placeholder as SetupPlaceholder } from './components/Setup';
import { WCPaySuggestion } from './components/WCPay';
+import { WCPayBNPLSuggestion } from './components/WCPayBNPL';
import { getCountryCode } from '~/dashboard/utils';
import {
getEnrichedPaymentGateways,
@@ -151,7 +152,12 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => {
getIsGatewayWCPay
) !== -1;
- const [ wcPayGateway, offlineGateways, additionalGateways ] = useMemo(
+ const [
+ wcPayGateway,
+ offlineGateways,
+ additionalGateways,
+ wcPayBnplGateway,
+ ] = useMemo(
() =>
getSplitGateways(
paymentGateways,
@@ -174,6 +180,9 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => {
if ( wcPayGateway.length ) {
shownGateways.push( wcPayGateway[ 0 ].id );
}
+ if ( wcPayBnplGateway.length ) {
+ shownGateways.push( wcPayBnplGateway[ 0 ].id );
+ }
if ( additionalGateways.length ) {
shownGateways = shownGateways.concat(
additionalGateways.map( ( g ) => g.id )
@@ -185,7 +194,7 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => {
} );
}
}
- }, [ additionalGateways, currentGateway, wcPayGateway ] );
+ }, [ additionalGateways, currentGateway, wcPayGateway, wcPayBnplGateway ] );
const trackSeeMore = () => {
recordEvent( 'tasklist_payment_see_more', {} );
@@ -244,7 +253,7 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => {
href="https://woocommerce.com/product-category/woocommerce-extensions/payment-gateways/?utm_source=payments_recommendations"
target="_blank"
onClick={ trackSeeMore }
- isTertiary
+ variant="tertiary"
>
{ __( 'See more', 'woocommerce' ) }
@@ -275,6 +284,11 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => {
) : (
<>
{ additionalSection }
+ { !! wcPayBnplGateway.length && (
+
+ ) }
{ offlineSection }
>
) }
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/test/index.js b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/test/index.js
index c5ce5bf8e37..c0228b95db4 100644
--- a/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/test/index.js
+++ b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/test/index.js
@@ -72,6 +72,16 @@ const paymentGatewaySuggestions = [
is_visible: true,
recommendation_priority: 1,
},
+ {
+ id: 'woocommerce_payments:bnpl',
+ title: 'Activate BNPL instantly on WooPayments',
+ content:
+ 'The world’s favorite buy now, pay later options and many more are right at your fingertips with WooPayments — all from one dashboard, without needing multiple extensions and logins.',
+ image: 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/wcpay-bnpl.svg',
+ plugins: [ 'woocommerce-payments' ],
+ is_visible: true,
+ recommendation_priority: 1,
+ },
{
id: 'eway',
title: 'Eway',
@@ -86,7 +96,7 @@ const paymentGatewaySuggestions = [
];
const paymentGatewaySuggestionsWithoutWCPay = paymentGatewaySuggestions.filter(
- ( p ) => p.title !== 'WooPayments'
+ ( p ) => ! p.id.startsWith( 'woocommerce_payments' )
);
describe( 'PaymentGatewaySuggestions', () => {
@@ -131,6 +141,11 @@ describe( 'PaymentGatewaySuggestions', () => {
)
.textContent.includes( 'WooPayments' )
).toBe( true );
+
+ // WCPay BNPL suggestion should not be shown since WCPay is shown.
+ expect(
+ container.querySelector( '.woocommerce-wcpay-bnpl-suggestion' )
+ ).toBeFalsy();
} );
test( 'should render all payment gateways except WCPay', () => {
@@ -172,7 +187,7 @@ describe( 'PaymentGatewaySuggestions', () => {
] );
} );
- test( 'should the payment gateway offline options at the bottom', () => {
+ test( 'should render the payment gateway offline options at the bottom', () => {
const onComplete = jest.fn();
const query = {};
useSelect.mockImplementation( () => ( {
@@ -215,6 +230,7 @@ describe( 'PaymentGatewaySuggestions', () => {
image: 'http://localhost:8888/wp-content/plugins/woocommerce/assets/images/paypal.png',
plugins: [ 'woocommerce-paypal-payments' ],
is_visible: true,
+ settings_url: 'http://example.com',
},
],
} ) );
@@ -229,7 +245,7 @@ describe( 'PaymentGatewaySuggestions', () => {
expect( getByText( 'Finish setup' ) ).toBeInTheDocument();
} );
- test( 'should show "category_additional" gateways after WCPay is set up', () => {
+ test( 'should show "category_additional" gateways and WCPay BNPL after WCPay is set up', () => {
const onComplete = jest.fn();
const query = {};
useSelect.mockImplementation( () => ( {
@@ -243,9 +259,10 @@ describe( 'PaymentGatewaySuggestions', () => {
plugins: [ 'woocommerce-payments' ],
is_visible: true,
needs_setup: false,
+ settings_url: 'http://example.com',
},
],
- countryCode: 'US',
+ countryCode: 'US', // Country with WCPay BNPL.
} ) );
const { container } = render(
@@ -273,6 +290,11 @@ describe( 'PaymentGatewaySuggestions', () => {
'Cash on delivery',
'Direct bank transfer',
] );
+
+ // WCPay BNPL suggestion should be shown.
+ expect(
+ container.querySelector( '.woocommerce-wcpay-bnpl-suggestion' )
+ ).toBeInTheDocument();
} );
test( 'should show "category_additional" gateways after a primary gateway (other than WCPay) is set up', () => {
@@ -291,6 +313,7 @@ describe( 'PaymentGatewaySuggestions', () => {
image: 'http://localhost:8888/wp-content/plugins/woocommerce/assets/images/paypal.png',
plugins: [ 'woocommerce-paypal-payments' ],
is_visible: true,
+ settings_url: 'http://example.com',
},
],
countryCode: 'US',
@@ -340,6 +363,7 @@ describe( 'PaymentGatewaySuggestions', () => {
image: 'http://localhost:8888/wp-content/plugins/woocommerce/assets/images/paypal.png',
plugins: [ 'woocommerce-paypal-payments' ],
is_visible: true,
+ settings_url: 'http://example.com',
},
],
} ) );
@@ -381,4 +405,41 @@ describe( 'PaymentGatewaySuggestions', () => {
recordEvent.mock.calls[ recordEvent.mock.calls.length - 1 ]
).toEqual( [ 'tasklist_payment_see_more', {} ] );
} );
+
+ test( 'should record event correctly when WCPay BNPL Get started is clicked', () => {
+ const onComplete = jest.fn();
+ const query = {};
+ useSelect.mockImplementation( () => ( {
+ isResolving: false,
+ getPaymentGateway: jest.fn(),
+ paymentGatewaySuggestions,
+ installedPaymentGateways: [
+ {
+ id: 'woocommerce_payments',
+ title: 'WooPayments',
+ plugins: [ 'woocommerce-payments' ],
+ is_visible: true,
+ needs_setup: false,
+ settings_url: 'http://example.com',
+ },
+ ],
+ countryCode: 'US', // Country with WCPay BNPL.
+ } ) );
+
+ const { container } = render(
+
+ );
+
+ fireEvent.click(
+ container.querySelector(
+ '.woocommerce-wcpay-bnpl-suggestion__button'
+ )
+ );
+ expect(
+ recordEvent.mock.calls[ recordEvent.mock.calls.length - 1 ]
+ ).toEqual( [ 'tasklist_payments_wcpay_bnpl_click' ] );
+ } );
} );
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/test/utils.js b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/test/utils.js
index 3db3b1fa663..c0c42dd0078 100644
--- a/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/test/utils.js
+++ b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/test/utils.js
@@ -4,11 +4,19 @@
import { getSplitGateways, getIsWCPayOrOtherCategoryDoneSetup } from '../utils';
const wcpay = {
+ id: 'woocommerce_payments:something',
plugins: [ 'woocommerce-payments' ],
installed: false,
needsSetup: true,
};
+const wcpayBnpl = {
+ id: 'woocommerce_payments:bnpl',
+ plugins: [ 'woocommerce-payments' ],
+ installed: true,
+ needsSetup: false,
+};
+
const cod = {
is_offline: true,
};
@@ -47,12 +55,13 @@ const amazonPay = {
describe( 'getSplitGateways()', () => {
it( 'Returns WCPay gateways', () => {
- const [ wcpayGateways ] = getSplitGateways(
- [ wcpay, cod, paypal ],
+ const [ wcpayGateways, , , wcpayBnplGateway ] = getSplitGateways(
+ [ wcpay, cod, paypal, wcpayBnpl ],
'US',
true
);
expect( wcpayGateways ).toEqual( [ wcpay ] );
+ expect( wcpayBnplGateway ).toEqual( [ wcpayBnpl ] );
} );
it( 'Returns offline gateways', () => {
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/utils.js b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/utils.js
index 0380a7270bc..e4f076024ba 100644
--- a/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/utils.js
+++ b/plugins/woocommerce-admin/client/task-lists/fills/PaymentGatewaySuggestions/utils.js
@@ -90,7 +90,7 @@ export const getIsWCPayOrOtherCategoryDoneSetup = (
* @param {string} countryCode Store country code.
* @param {boolean} isWCPaySupported Whether WCPay is supported in the store.
* @param {boolean} isWCPayOrOtherCategoryDoneSetup Whether WCPay or "other" category gateway is done setup.
- * @return {Array} Array of [ WCPay, offline, main list ].
+ * @return {Array} Array of [ WCPay, offline, main list, WCPayBNPL ].
*/
export const getSplitGateways = (
paymentGateways,
@@ -103,17 +103,28 @@ export const getSplitGateways = (
.reduce(
( all, gateway ) => {
// mainList is the list of gateways that is shown in the payments task.
- const [ wcPay, offline, mainList ] = all;
+ const [ wcPay, offline, mainList, wcPayBnpl ] = all;
+ // Handle WCPay-related gateways.
if ( getIsGatewayWCPay( gateway ) ) {
- if (
- isWCPaySupported &&
- ! ( gateway.installed && ! gateway.needsSetup )
- ) {
- // WCPay is always shown when it's installed but not setup.
- wcPay.push( gateway );
+ if ( isWCPaySupported ) {
+ // If we encounter the special WCPay BNPL gateway, we handle it separately and
+ // not let it be added to the regular WCPay list.
+ if ( gateway.id === 'woocommerce_payments:bnpl' ) {
+ // WCPay BNPL is only shown when WCPay is installed and setup.
+ // It should be mutually exclusive with WCPay.
+ if ( gateway.installed && ! gateway.needsSetup ) {
+ wcPayBnpl.push( gateway );
+ }
+ } else if (
+ ! ( gateway.installed && ! gateway.needsSetup )
+ ) {
+ // WCPay is always shown when it's not installed, or it's installed but needs setup.
+ wcPay.push( gateway );
+ }
}
- // WCPay is ignored if it reaches here.
+
+ // The WCPay-related gateway is ignored if it reaches here.
} else if ( gateway.is_offline ) {
// Offline gateways are always shown.
offline.push( gateway );
@@ -137,5 +148,5 @@ export const getSplitGateways = (
return all;
},
- [ [], [], [] ]
+ [ [], [], [], [] ]
);
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/constants.tsx b/plugins/woocommerce-admin/client/task-lists/fills/products/constants.tsx
index 466f3a8efcb..1dd3bac2ee5 100644
--- a/plugins/woocommerce-admin/client/task-lists/fills/products/constants.tsx
+++ b/plugins/woocommerce-admin/client/task-lists/fills/products/constants.tsx
@@ -6,8 +6,8 @@ import ProductIcon from 'gridicons/dist/product';
import CloudOutlineIcon from 'gridicons/dist/cloud-outline';
import TypesIcon from 'gridicons/dist/types';
import { Icon, chevronRight } from '@wordpress/icons';
-import { addFilter } from '@wordpress/hooks';
import { recordEvent } from '@woocommerce/tracks';
+import { getAdminLink } from '@woocommerce/settings';
/**
* Internal dependencies
@@ -16,6 +16,7 @@ import Link from './icon/link_24px.js';
import Widget from './icon/widgets_24px.js';
import LightBulb from './icon/lightbulb_24px.js';
import PrintfulIcon from './icon/printful.png';
+import Upload from './icon/upload_40px.js';
export const productTypes = Object.freeze( [
{
@@ -90,7 +91,7 @@ export const PrintfulAdvertProductPlacement = {
'Design and easily sell custom print products online with Printful.',
'woocommerce'
),
- className: 'woocommerce-products-list__item-printful-advert',
+ className: 'woocommerce-products-list__item-advert',
before: (
+ { __( 'Are you already selling somewhere else?', 'woocommerce' ) }
+
+ ),
+ content: __( 'Import your products from a CSV file.', 'woocommerce' ),
+ className: 'woocommerce-products-list__item-advert',
+ before: ,
+ after: ,
+ onClick: () => {
+ recordEvent( 'tasklist_add_product', {
+ method: 'import',
+ } );
+ window.location.href = getAdminLink(
+ 'edit.php?post_type=product&page=product_importer&wc_onboarding_active_task=products'
+ );
+ },
+};
+
+export type SponsoredProductPlacementType =
+ | typeof PrintfulAdvertProductPlacement
+ | typeof ImportCSVItem;
export type ProductType =
| ( typeof productTypes )[ number ]
| typeof LoadSampleProductType
- | typeof SponsoredProductPlacementType;
+ | SponsoredProductPlacementType;
export type ProductTypeKey = ProductType[ 'key' ];
export const onboardingProductTypesToSurfaced: Readonly<
@@ -131,13 +155,3 @@ export const SETUP_TASKLIST_PRODUCT_TYPES_FILTER =
export const SETUP_TASKLIST_PRODUCTS_AFTER_FILTER =
'woocommerce_admin_task_products_after';
-
-if ( window.wcAdminFeatures && window.wcAdminFeatures.printful === true ) {
- addFilter(
- SETUP_TASKLIST_PRODUCTS_AFTER_FILTER,
- 'woocommerce/task-lists/products-sponsored-placement',
- ( products ) => {
- return [ ...products, PrintfulAdvertProductPlacement ];
- }
- );
-}
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/footer.tsx b/plugins/woocommerce-admin/client/task-lists/fills/products/footer.tsx
deleted file mode 100644
index 98739bf6c5d..00000000000
--- a/plugins/woocommerce-admin/client/task-lists/fills/products/footer.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * External dependencies
- */
-import { __ } from '@wordpress/i18n';
-import interpolateComponents from '@automattic/interpolate-components';
-import { Text } from '@woocommerce/experimental';
-import { Link } from '@woocommerce/components';
-import { getAdminLink } from '@woocommerce/settings';
-import { recordEvent } from '@woocommerce/tracks';
-
-/**
- * Internal dependencies
- */
-import useRecordCompletionTime from '../use-record-completion-time';
-
-const Footer: React.FC = () => {
- const { recordCompletionTime } = useRecordCompletionTime( 'products' );
-
- return (
-
-
- { __(
- 'Are you already selling somewhere else?',
- 'woocommerce'
- ) }
-
-
- { interpolateComponents( {
- mixedString: __(
- '{{importCSVLink}}Import your products from a CSV file{{/importCSVLink}}.',
- 'woocommerce'
- ),
- components: {
- importCSVLink: (
- {
- recordEvent( 'tasklist_add_product', {
- method: 'import',
- } );
- recordCompletionTime();
- window.location.href = getAdminLink(
- 'edit.php?post_type=product&page=product_importer&wc_onboarding_active_task=products'
- );
- return false;
- } }
- href=""
- type="wc-admin"
- >
- <>>
-
- ),
- },
- } ) }
-
-
- );
-};
-
-export default Footer;
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/icon/upload_40px.js b/plugins/woocommerce-admin/client/task-lists/fills/products/icon/upload_40px.js
new file mode 100644
index 00000000000..2724fd73b99
--- /dev/null
+++ b/plugins/woocommerce-admin/client/task-lists/fills/products/icon/upload_40px.js
@@ -0,0 +1,35 @@
+const Upload = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default Upload;
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/index.scss b/plugins/woocommerce-admin/client/task-lists/fills/products/index.scss
index 6f966a4c441..9c68a5de2d9 100644
--- a/plugins/woocommerce-admin/client/task-lists/fills/products/index.scss
+++ b/plugins/woocommerce-admin/client/task-lists/fills/products/index.scss
@@ -53,7 +53,18 @@
.woocommerce-products-stack {
max-width: 550px;
- margin-top: 32px;
+ margin-top: 24px;
+
+ &:first-child {
+ margin-top: 32px;
+ }
+ }
+
+ .woocommerce-list__item-before > svg {
+ fill: var(--wp-admin-theme-color);
+ .stroke-admin-theme {
+ stroke: var(--wp-admin-theme-color);
+ }
}
}
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/index.tsx b/plugins/woocommerce-admin/client/task-lists/fills/products/index.tsx
index 231ab43a0e9..83b5b9277e8 100644
--- a/plugins/woocommerce-admin/client/task-lists/fills/products/index.tsx
+++ b/plugins/woocommerce-admin/client/task-lists/fills/products/index.tsx
@@ -20,12 +20,15 @@ import { getAdminSetting } from '~/utils/admin-settings';
import { getSurfacedProductTypeKeys, getProductTypes } from './utils';
import useProductTypeListItems from './use-product-types-list-items';
import Stack from './stack';
-import Footer from './footer';
import LoadSampleProductModal from '../components/load-sample-product-modal';
import useLoadSampleProducts from '../components/use-load-sample-products';
import LoadSampleProductConfirmModal from '../components/load-sample-product-confirm-modal';
import useRecordCompletionTime from '../use-record-completion-time';
-import { SETUP_TASKLIST_PRODUCTS_AFTER_FILTER } from './constants';
+import {
+ SETUP_TASKLIST_PRODUCTS_AFTER_FILTER,
+ ImportCSVItem,
+ PrintfulAdvertProductPlacement,
+} from './constants';
const getOnboardingProductType = (): string[] => {
const onboardingData = getAdminSetting( 'onboarding' );
@@ -72,7 +75,7 @@ export const Products = () => {
() =>
productTypes.map( ( productType ) => ( {
...productType,
- onClick: () => {
+ onClick: (): void => {
productType.onClick();
recordCompletionTime();
},
@@ -113,6 +116,27 @@ export const Products = () => {
return surfacedProductTypesAndAppendedProducts;
}, [ surfacedProductTypeKeys, isExpanded, productTypesWithTimeRecord ] );
+ const footerStack = useMemo( () => {
+ const options = [];
+ const importCSVItemWithTimeRecord = {
+ ...ImportCSVItem,
+ onClick: () => {
+ ImportCSVItem.onClick();
+ recordCompletionTime();
+ },
+ };
+
+ options.push( importCSVItemWithTimeRecord );
+
+ if (
+ window.wcAdminFeatures &&
+ window.wcAdminFeatures.printful === true
+ ) {
+ options.push( PrintfulAdvertProductPlacement );
+ }
+ return options;
+ }, [ recordCompletionTime ] );
+
return (
{
setIsExpanded( ! isExpanded );
} }
/>
-
+
{ isLoadingSampleProducts ? (
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/stack.scss b/plugins/woocommerce-admin/client/task-lists/fills/products/stack.scss
index 62f07689d3d..13ad6bd76ff 100644
--- a/plugins/woocommerce-admin/client/task-lists/fills/products/stack.scss
+++ b/plugins/woocommerce-admin/client/task-lists/fills/products/stack.scss
@@ -44,7 +44,7 @@
margin-top: 0;
}
- &.woocommerce-products-list__item-printful-advert {
+ &.woocommerce-products-list__item-advert {
.woocommerce-list__item-before {
background: none;
border-radius: 0;
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/stack.tsx b/plugins/woocommerce-admin/client/task-lists/fills/products/stack.tsx
index 1ccf5aaa454..d827749d33a 100644
--- a/plugins/woocommerce-admin/client/task-lists/fills/products/stack.tsx
+++ b/plugins/woocommerce-admin/client/task-lists/fills/products/stack.tsx
@@ -15,7 +15,9 @@ import { ProductType } from './constants';
import './stack.scss';
import useRecordCompletionTime from '../use-record-completion-time';
-type StackProps = {
+type StackProps = StackWithLoadSampleBlurb | StackWithoutText;
+
+type StackWithLoadSampleBlurb = {
items: ( ProductType & {
onClick: () => void;
} )[];
@@ -24,9 +26,18 @@ type StackProps = {
isTaskListItemClicked?: boolean;
};
+type StackWithoutText = {
+ items: ( ProductType & {
+ onClick: () => void;
+ } )[];
+ showOtherOptions: false;
+ onClickLoadSampleProduct?: () => void;
+ isTaskListItemClicked?: boolean;
+};
+
const Stack: React.FC< StackProps > = ( {
items,
- onClickLoadSampleProduct,
+ onClickLoadSampleProduct = () => {},
showOtherOptions = true,
isTaskListItemClicked = false,
} ) => {
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/test/footer.tsx b/plugins/woocommerce-admin/client/task-lists/fills/products/test/footer.tsx
deleted file mode 100644
index b75e20d3107..00000000000
--- a/plugins/woocommerce-admin/client/task-lists/fills/products/test/footer.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * External dependencies
- */
-import { render } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { recordEvent } from '@woocommerce/tracks';
-
-jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
-
-/**
- * Internal dependencies
- */
-import Footer from '../footer';
-
-describe( 'Footer', () => {
- beforeEach( () => {
- ( recordEvent as jest.Mock ).mockClear();
- } );
- it( 'should render footer with one links', () => {
- const { queryAllByRole } = render( );
- expect( queryAllByRole( 'link' ) ).toHaveLength( 1 );
- } );
-
- it( 'clicking on import CSV should fire event tasklist_add_product with method:import and task_completion_time', () => {
- const { getByText } = render( );
- userEvent.click( getByText( 'Import your products from a CSV file' ) );
- expect( recordEvent ).toHaveBeenNthCalledWith(
- 1,
- 'tasklist_add_product',
- { method: 'import' }
- );
- expect( recordEvent ).toHaveBeenNthCalledWith(
- 2,
- 'task_completion_time',
- { task_name: 'products', time: '0-2s' }
- );
- } );
-} );
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/test/index.tsx b/plugins/woocommerce-admin/client/task-lists/fills/products/test/index.tsx
index de7d916a5a8..eaf725ef5e1 100644
--- a/plugins/woocommerce-admin/client/task-lists/fills/products/test/index.tsx
+++ b/plugins/woocommerce-admin/client/task-lists/fills/products/test/index.tsx
@@ -72,10 +72,11 @@ describe( 'Products', () => {
product_types: [ 'downloads' ],
},
} ) );
- const { queryByText, queryByRole } = render( );
+ const { queryByText, queryAllByRole } = render( );
+ const productTypeList = queryAllByRole( 'menu' )?.[ 0 ];
expect( queryByText( 'Digital product' ) ).toBeInTheDocument();
- expect( queryByRole( 'menu' )?.childElementCount ).toBe( 1 );
+ expect( productTypeList?.childElementCount ).toBe( 1 );
expect( queryByText( 'View more product types' ) ).toBeInTheDocument();
} );
@@ -116,7 +117,9 @@ describe( 'Products', () => {
product_types: [ 'downloads' ],
},
} ) );
- const { queryByText, getByRole, queryByRole } = render( );
+ const { queryByText, getByRole, queryAllByRole } = render(
+
+ );
expect( queryByText( 'View more product types' ) ).toBeInTheDocument();
@@ -124,11 +127,13 @@ describe( 'Products', () => {
getByRole( 'button', { name: 'View more product types' } )
);
- await waitFor( () =>
- expect( queryByRole( 'menu' )?.childElementCount ).toBe(
+ await waitFor( () => {
+ const productTypeList = queryAllByRole( 'menu' )?.[ 0 ];
+ expect( productTypeList?.childElementCount ).toBe(
productTypes.length
- )
- );
+ );
+ } );
+
userEvent.click(
getByRole( 'menuitem', {
name: 'Grouped product A collection of related products.',
@@ -162,7 +167,9 @@ describe( 'Products', () => {
product_types: [ 'downloads' ],
},
} ) );
- const { queryByText, getByRole, queryByRole } = render( );
+ const { queryByText, getByRole, queryAllByRole } = render(
+
+ );
expect( queryByText( 'View more product types' ) ).toBeInTheDocument();
@@ -170,11 +177,12 @@ describe( 'Products', () => {
getByRole( 'button', { name: 'View more product types' } )
);
- await waitFor( () =>
- expect( queryByRole( 'menu' )?.childElementCount ).toBe(
+ await waitFor( () => {
+ const productTypeList = queryAllByRole( 'menu' )?.[ 0 ];
+ expect( productTypeList?.childElementCount ).toBe(
productTypes.length
- )
- );
+ );
+ } );
expect( queryByText( 'View less product types' ) ).toBeInTheDocument();
} );
@@ -237,8 +245,10 @@ describe( 'Products', () => {
it( 'should render stacked layout', async () => {
const { container } = render( );
+
expect(
container.getElementsByClassName( 'woocommerce-products-stack' )
- ).toHaveLength( 1 );
+ .length
+ ).toBeGreaterThanOrEqual( 1 );
} );
} );
diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/use-create-product-by-type.ts b/plugins/woocommerce-admin/client/task-lists/fills/products/use-create-product-by-type.ts
index a224abf33f4..54ac34cdfc0 100644
--- a/plugins/woocommerce-admin/client/task-lists/fills/products/use-create-product-by-type.ts
+++ b/plugins/woocommerce-admin/client/task-lists/fills/products/use-create-product-by-type.ts
@@ -3,7 +3,7 @@
*/
import { useDispatch } from '@wordpress/data';
import { ITEMS_STORE_NAME } from '@woocommerce/data';
-import { getNewPath, navigateTo } from '@woocommerce/navigation';
+import { navigateTo } from '@woocommerce/navigation';
import { getAdminLink } from '@woocommerce/settings';
import { loadExperimentAssignment } from '@woocommerce/explat';
import { useState } from '@wordpress/element';
@@ -22,19 +22,7 @@ export const useCreateProductByType = () => {
const { createProductFromTemplate } = useDispatch( ITEMS_STORE_NAME );
const [ isRequesting, setIsRequesting ] = useState< boolean >( false );
- const getProductEditPageLink = async (
- type: ProductTypeKey,
- classicEditor: boolean
- ) => {
- if (
- type === 'physical' ||
- type === 'variable' ||
- type === 'digital'
- ) {
- return classicEditor
- ? getAdminLink( 'post-new.php?post_type=product' )
- : getNewPath( {}, '/add-product', {} );
- }
+ const getProductEditPageLink = async ( type: ProductTypeKey ) => {
try {
const data: {
id?: number;
@@ -46,11 +34,9 @@ export const useCreateProductByType = () => {
{ _fields: [ 'id' ] }
);
if ( data && data.id ) {
- return classicEditor
- ? getAdminLink(
- `post.php?post=${ data.id }&action=edit&wc_onboarding_active_task=products&tutorial=true&tutorial_type=${ type }`
- )
- : getNewPath( {}, '/product/' + data.id, {} );
+ return getAdminLink(
+ `post.php?post=${ data.id }&action=edit&wc_onboarding_active_task=products&tutorial=true&tutorial_type=${ type }`
+ );
}
throw new Error( 'Unexpected empty data response from server' );
} catch ( error ) {
@@ -72,7 +58,7 @@ export const useCreateProductByType = () => {
EXPERIMENT_NAME
);
if ( assignment.variationName === 'treatment' ) {
- const url = await getProductEditPageLink( type, true );
+ const url = await getProductEditPageLink( type );
const _feature_nonce = getAdminSetting( '_feature_nonce' );
window.location.href =
url +
@@ -81,7 +67,7 @@ export const useCreateProductByType = () => {
}
}
- const url = await getProductEditPageLink( type, true );
+ const url = await getProductEditPageLink( type );
if ( url ) {
navigateTo( { url } );
}
diff --git a/plugins/woocommerce-admin/client/task-lists/reminder-bar/reminder-bar.scss b/plugins/woocommerce-admin/client/task-lists/reminder-bar/reminder-bar.scss
index 9a9579a312b..6f3d4b1c30e 100644
--- a/plugins/woocommerce-admin/client/task-lists/reminder-bar/reminder-bar.scss
+++ b/plugins/woocommerce-admin/client/task-lists/reminder-bar/reminder-bar.scss
@@ -5,6 +5,7 @@
justify-content: space-between;
align-items: center;
color: #fff;
+ padding: 0 $gap-small;
&::before {
content: "";
diff --git a/plugins/woocommerce-admin/client/typings/global.d.ts b/plugins/woocommerce-admin/client/typings/global.d.ts
index 125a8aa3757..4caf97d3849 100644
--- a/plugins/woocommerce-admin/client/typings/global.d.ts
+++ b/plugins/woocommerce-admin/client/typings/global.d.ts
@@ -59,6 +59,7 @@ declare global {
'shipping-smart-defaults': boolean;
'shipping-setting-tour': boolean;
'launch-your-store': boolean;
+ 'reactify-classic-payments-settings': boolean;
};
wp: {
updates?: {
diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/beta-features-tracking-modal/index.js b/plugins/woocommerce-admin/client/wp-admin-scripts/beta-features-tracking-modal/index.js
index 222d8400b56..1ae8edf77d5 100644
--- a/plugins/woocommerce-admin/client/wp-admin-scripts/beta-features-tracking-modal/index.js
+++ b/plugins/woocommerce-admin/client/wp-admin-scripts/beta-features-tracking-modal/index.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { render } from '@wordpress/element';
+import { createRoot } from '@wordpress/element';
/**
* Internal dependencies
@@ -10,9 +10,8 @@ import { BetaFeaturesTrackingContainer } from './container';
import './style.scss';
const betaFeaturesRoot = document.createElement( 'div' );
-betaFeaturesRoot.setAttribute( 'id', 'beta-features-tracking' );
-render(
- ,
- document.body.appendChild( betaFeaturesRoot )
+betaFeaturesRoot.setAttribute( 'id', 'beta-features-tracking' );
+createRoot( document.body.appendChild( betaFeaturesRoot ) ).render(
+
);
diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/payment-method-promotions/index.tsx b/plugins/woocommerce-admin/client/wp-admin-scripts/payment-method-promotions/index.tsx
index db54694ab4b..d492b46b9b7 100644
--- a/plugins/woocommerce-admin/client/wp-admin-scripts/payment-method-promotions/index.tsx
+++ b/plugins/woocommerce-admin/client/wp-admin-scripts/payment-method-promotions/index.tsx
@@ -1,7 +1,9 @@
/**
* External dependencies
*/
-import { render } from '@wordpress/element';
+// @ts-expect-error -- @wordpress/element doesn't export createRoot until WP6.2
+// eslint-disable-next-line @woocommerce/dependency-group
+import { createRoot } from '@wordpress/element';
/**
* Internal dependencies
@@ -34,7 +36,7 @@ PAYMENT_METHOD_PROMOTIONS.forEach( ( paymentMethod ) => {
);
const subTitle = container.getElementsByClassName( 'gateway-subtitle' );
- render(
+ createRoot( container ).render(
{
subTitleContent={
subTitle.length === 1 ? subTitle[ 0 ].innerHTML : undefined
}
- />,
- container
+ />
);
}
} );
diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/payment-method-promotions/payment-promotion-row.tsx b/plugins/woocommerce-admin/client/wp-admin-scripts/payment-method-promotions/payment-promotion-row.tsx
index c35811a7ee1..888bc2137c7 100644
--- a/plugins/woocommerce-admin/client/wp-admin-scripts/payment-method-promotions/payment-promotion-row.tsx
+++ b/plugins/woocommerce-admin/client/wp-admin-scripts/payment-method-promotions/payment-promotion-row.tsx
@@ -12,11 +12,13 @@ import { recordEvent } from '@woocommerce/tracks';
import { useDispatch, useSelect } from '@wordpress/data';
import { sanitize } from 'dompurify';
import { __ } from '@wordpress/i18n';
+import { WooPaymentMethodsLogos } from '@woocommerce/onboarding';
/**
* Internal dependencies
*/
import './payment-promotion-row.scss';
+import { getAdminSetting } from '~/utils/admin-settings';
function sanitizeHTML( html: string ) {
return {
@@ -116,6 +118,8 @@ export const PaymentPromotionRow: React.FC< PaymentPromotionRowProps > = ( {
return null;
}
+ const isWooPayEligible = getAdminSetting( 'isWooPayEligible', false );
+
return (
<>
{ columns.map( ( column ) => {
@@ -131,7 +135,20 @@ export const PaymentPromotionRow: React.FC< PaymentPromotionRowProps > = ( {
>
{ title }
- { subTitleContent ? (
+ { gatewayId ===
+ 'pre_install_woocommerce_payments_promotion' && (
+
+
+
+ ) }
+ { gatewayId !==
+ 'pre_install_woocommerce_payments_promotion' &&
+ subTitleContent ? (
(
);
if ( shippingZoneRegionPickerRoot ) {
- if ( createRoot ) {
- createRoot( shippingZoneRegionPickerRoot ).render(
);
- } else {
- render(
, shippingZoneRegionPickerRoot );
- }
+ createRoot( shippingZoneRegionPickerRoot ).render(
);
}
diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/actions.js b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/actions.js
new file mode 100644
index 00000000000..ac40bab0a73
--- /dev/null
+++ b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/actions.js
@@ -0,0 +1,55 @@
+/**
+ * External dependencies
+ */
+import { addQueryArgs } from '@wordpress/url';
+import { getSetting } from '@woocommerce/settings';
+import apiFetch from '@wordpress/api-fetch';
+
+const request = ( { action, productId, nonce }, callback ) => {
+ const url = addQueryArgs(
+ new URL( 'admin-ajax.php', getSetting( 'adminUrl' ) ).toString(),
+ {
+ action,
+ product_id: productId,
+ _ajax_nonce: nonce,
+ }
+ );
+
+ const headers = {
+ 'Content-Type': 'application/json',
+ };
+
+ apiFetch( {
+ url,
+ method: 'GET',
+ headers,
+ } ).then( ( response ) => {
+ if ( callback ) {
+ callback( response );
+ }
+ } );
+};
+
+export const dismissRequest = (
+ { dismissAction, productId, dismissNonce },
+ callback
+) => {
+ const args = {
+ action: dismissAction,
+ productId,
+ nonce: dismissNonce,
+ };
+ request( args, callback );
+};
+
+export const remindLaterRequest = (
+ { remindLaterAction, productId, remindLaterNonce },
+ callback
+) => {
+ const args = {
+ action: remindLaterAction,
+ productId,
+ nonce: remindLaterNonce,
+ };
+ request( args, callback );
+};
diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/illustration.svg b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/illustration.svg
new file mode 100644
index 00000000000..cbdb0cc06c3
--- /dev/null
+++ b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/illustration.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/index.js b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/index.js
new file mode 100644
index 00000000000..dc4e0670e88
--- /dev/null
+++ b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/index.js
@@ -0,0 +1,46 @@
+/**
+ * External dependencies
+ */
+import { render } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import ProductUsageNoticeModal from './modal';
+import './style.scss';
+
+const {
+ renewUrl,
+ subscribeUrl,
+ productId,
+ productName,
+ productRegularPrice,
+ dismissAction,
+ dismissNonce,
+ remindLaterAction,
+ remindLaterNonce,
+ colorScheme,
+ subscriptionState,
+ screenId,
+} = window.wooProductUsageNotice;
+
+const container = document.createElement( 'div' );
+container.setAttribute( 'id', 'woo-product-usage-notice' );
+
+render(
+
,
+ document.body.appendChild( container )
+);
diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/modal.js b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/modal.js
new file mode 100644
index 00000000000..575d1de0318
--- /dev/null
+++ b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/modal.js
@@ -0,0 +1,258 @@
+/**
+ * External dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+import { useEffect, useState } from '@wordpress/element';
+import {
+ Button,
+ Card,
+ CardBody,
+ CardFooter,
+ CardHeader,
+ CardMedia,
+ Flex,
+ FlexItem,
+ Icon,
+ Modal,
+ ResponsiveWrapper,
+} from '@wordpress/components';
+import { commentContent, people, reusableBlock } from '@wordpress/icons';
+import { Text } from '@woocommerce/experimental';
+import { recordEvent } from '@woocommerce/tracks';
+
+/**
+ * Internal dependencies
+ */
+import extensionsSvg from './illustration.svg';
+import { dismissRequest, remindLaterRequest } from './actions';
+
+export default function ProductUsageNoticeModal( {
+ renewUrl,
+ subscribeUrl,
+ productId,
+ productName,
+ productRegularPrice,
+ dismissAction,
+ dismissNonce,
+ remindLaterAction,
+ remindLaterNonce,
+ subscriptionState,
+ screenId,
+} ) {
+ const [ isModalOpen, setIsModalOpen ] = useState( true );
+
+ useEffect( () => {
+ if ( isModalOpen ) {
+ recordEvent( 'product_usage_notice_opened', {
+ product_id: productId,
+ screen_id: screenId,
+ } );
+ }
+ }, [ isModalOpen, productId, screenId ] );
+
+ const isExpired = subscriptionState.expired;
+
+ const dismiss = () => {
+ dismissRequest(
+ {
+ dismissAction,
+ productId,
+ dismissNonce,
+ },
+ () => {
+ setIsModalOpen( false );
+ recordEvent( 'product_usage_notice_dismissed', {
+ product_id: productId,
+ screen_id: screenId,
+ } );
+ }
+ );
+ };
+ const remindLater = () => {
+ remindLaterRequest(
+ {
+ remindLaterAction,
+ productId,
+ remindLaterNonce,
+ },
+ () => {
+ setIsModalOpen( false );
+ recordEvent( 'product_usage_notice_maybe_later_clicked', {
+ product_id: productId,
+ screen_id: screenId,
+ } );
+ }
+ );
+ };
+ const renew = () => {
+ setIsModalOpen( false );
+ recordEvent( 'product_usage_notice_renew_clicked', {
+ product_id: productId,
+ screen_id: screenId,
+ } );
+ };
+ const subscribe = () => {
+ setIsModalOpen( false );
+ recordEvent( 'product_usage_notice_subscribe_clicked', {
+ product_id: productId,
+ screen_id: screenId,
+ } );
+ };
+
+ const renderBenefits = () => {
+ const subtitle = isExpired
+ ? __(
+ 'Reactivate your subscription and benefit from:',
+ 'woocommerce'
+ )
+ : __( 'Purchase a subscription to benefit from:', 'woocommerce' );
+
+ const benefits = [
+ {
+ key: 'get-updates',
+ icon: reusableBlock,
+ title: __( 'Improvements and security updates', 'woocommerce' ),
+ content: __(
+ 'Access the latest features and product updates.',
+ 'woocommerce'
+ ),
+ },
+ {
+ key: 'get-supports',
+ icon: commentContent,
+ title: __( 'Help when you need it', 'woocommerce' ),
+ content: __(
+ 'Get streamlined support from our global support team.',
+ 'woocommerce'
+ ),
+ },
+ {
+ key: 'supporting-ecosystem',
+ icon: people,
+ title: __( 'Supporting the ecosystem', 'woocommerce' ),
+ content: __(
+ 'A subscription helps us to continuously improve your extensions, themes, and WooCommerce experience.',
+ 'woocommerce'
+ ),
+ },
+ ];
+
+ return (
+
+
{ subtitle }
+
+ { benefits.map( ( { key, icon, title, content } ) => (
+
+
+
+
+
+
+
+ { title }
+
+ { content }
+
+
+ ) ) }
+
+ );
+ };
+
+ const renderPrimaryCard = () => {
+ const status = isExpired
+ ? __( 'Expired', 'woocommerce' )
+ : __( 'Unregistered', 'woocommerce' );
+
+ const title = isExpired
+ ? sprintf(
+ /* translators: %s is product name */
+ __( 'Renew %s', 'woocommerce' ),
+ productName
+ )
+ : sprintf(
+ /* translators: %s is product name */
+ __( 'Subscribe to %s', 'woocommerce' ),
+ productName
+ );
+
+ const buttonLabel = isExpired
+ ? sprintf(
+ /* translators: %s is product price */
+ __( 'Renew for $%s', 'woocommerce' ),
+ productRegularPrice
+ )
+ : sprintf(
+ /* translators: %s is product price */
+ __( 'Subscribe for $%s', 'woocommerce' ),
+ productRegularPrice
+ );
+
+ return (
+
+
+
+
+ { status }
+
+
+ { title }
+
+ { renderBenefits() }
+
+
+ { __( 'Maybe later', 'woocommerce' ) }
+
+
+ ( isExpired ? renew() : subscribe() ) }
+ >
+ { buttonLabel }
+
+
+
+ );
+ };
+
+ const renderSecondaryCard = () => {
+ return (
+
+
+
+
+
+
+
+ );
+ };
+
+ if ( ! isModalOpen ) {
+ return null;
+ }
+
+ return (
+
+
+ { renderPrimaryCard() }
+ { renderSecondaryCard() }
+
+
+ );
+}
diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/style.scss b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/style.scss
new file mode 100644
index 00000000000..5fe3e353178
--- /dev/null
+++ b/plugins/woocommerce-admin/client/wp-admin-scripts/woo-product-usage-notice/style.scss
@@ -0,0 +1,136 @@
+$font-sf-pro-text: helveticaneue-light, "Helvetica Neue Light",
+ "Helvetica Neue", sans-serif;
+
+.woocommerce-product-usage-notice {
+ border-radius: none;
+ font-family: $font-sf-pro-text;
+
+ .components-modal {
+ &__content {
+ margin: 0;
+ padding: 0;
+ }
+ &__header {
+ height: 0;
+ padding: 0;
+
+ .components-button {
+ top: 40px;
+ left: -20px;
+ }
+ }
+ }
+
+ .components-card {
+ box-shadow: none;
+
+ &.primary {
+ padding: 40px;
+ max-width: 468px;
+ }
+
+ &.secondary {
+ background-color: #faf7f3;
+ height: 100%;
+ min-width: 392px;
+ }
+
+ .subscription-status {
+ border-radius: 2px;
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 16px;
+ padding: 6px;
+
+ &__expired {
+ color: var(--wp-red-red-70, #8a2424);
+ background: var(--wp-red-red-0, #fcf0f1);
+ }
+ }
+
+ &__header,
+ &__body,
+ &__footer {
+ border: none;
+ display: flex;
+ gap: $gap;
+ padding: 0;
+ }
+
+ &__header {
+ align-items: stretch;
+ flex-direction: column;
+ padding-bottom: 40px;
+ h2 {
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 35px;
+ margin: 0;
+ }
+ }
+ &__body {
+ padding-bottom: 40px;
+ h3 {
+ font-size: 16px;
+ line-height: 24px;
+ margin: 0;
+ }
+ }
+ &__footer {
+ justify-content: right;
+
+ .is-secondary,
+ .is-secondary:hover {
+ box-shadow: none;
+ }
+ }
+
+ &__media {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ justify-content: center;
+ }
+ }
+}
+
+.woocommerce-subscription-benefits {
+ display: flex;
+ flex-direction: column;
+ gap: $gap-large;
+
+ &__item {
+ display: flex;
+ flex-direction: row;
+ gap: $gap;
+ text-decoration: none;
+ }
+
+ &__icon {
+ display: flex;
+ align-items: baseline;
+ padding: 0;
+ svg {
+ fill: #1e1e1e;
+ }
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ gap: $gap-smallest;
+ }
+ &__content h4 {
+ color: #1e1e1e;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 20px;
+ }
+ &__content p {
+ color: #757575;
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 16px;
+ }
+}
diff --git a/plugins/woocommerce-admin/webpack.config.js b/plugins/woocommerce-admin/webpack.config.js
index b1c965bd0ce..649d22749c3 100644
--- a/plugins/woocommerce-admin/webpack.config.js
+++ b/plugins/woocommerce-admin/webpack.config.js
@@ -10,8 +10,6 @@ const BundleAnalyzerPlugin =
const MomentTimezoneDataPlugin = require( 'moment-timezone-data-webpack-plugin' );
const ForkTsCheckerWebpackPlugin = require( 'fork-ts-checker-webpack-plugin' );
const ReactRefreshWebpackPlugin = require( '@pmmmwh/react-refresh-webpack-plugin' );
-const NormalModuleReplacementPlugin =
- require( 'webpack' ).NormalModuleReplacementPlugin;
/**
* Internal dependencies
@@ -78,6 +76,7 @@ const wpAdminScripts = [
'woo-enable-autorenew',
'woo-renew-subscription',
'woo-subscriptions-notice',
+ 'woo-product-usage-notice',
];
const getEntryPoints = () => {
const entryPoints = {
@@ -127,8 +126,8 @@ const webpackConfig = {
amd: false,
},
exclude: [
- // Exclude node_modules/.pnpm
- /node_modules(\/|\\)\.pnpm(\/|\\)/,
+ /[\/\\]node_modules[\/\\]\.pnpm[\/\\]/,
+ /[\/\\](changelog|bin|build|docs|test)[\/\\]/,
],
use: {
loader: 'babel-loader',
@@ -152,6 +151,11 @@ const webpackConfig = {
isHot &&
require.resolve( 'react-refresh/babel' ),
].filter( Boolean ),
+ cacheDirectory: path.resolve(
+ __dirname,
+ '../../node_modules/.cache/babel-loader'
+ ),
+ cacheCompression: false,
},
},
},
@@ -178,14 +182,6 @@ const webpackConfig = {
},
},
plugins: [
- // Workaround for Gutenberg private API consent string differences between WP 6.3 and 6.4+
- // The modified version checks for the WP version and replaces the consent string with the correct one.
- // This can be removed once we drop support for WP 6.3 in the "Customize Your Store" task.
- // See this PR for details: https://github.com/woocommerce/woocommerce/pull/40884
- new NormalModuleReplacementPlugin(
- /@wordpress\/edit-site\/build-module\/lock-unlock\.js/,
- path.resolve( __dirname, 'bin/modified-editsite-lock-unlock.js' )
- ),
...styleConfig.plugins,
// Runs TypeScript type checker on a separate process.
! process.env.STORYBOOK && new ForkTsCheckerWebpackPlugin(),
diff --git a/plugins/woocommerce-beta-tester/api/api.php b/plugins/woocommerce-beta-tester/api/api.php
index 4c0071e99a2..62a4b4df2af 100644
--- a/plugins/woocommerce-beta-tester/api/api.php
+++ b/plugins/woocommerce-beta-tester/api/api.php
@@ -53,12 +53,13 @@ require 'tools/delete-all-products.php';
require 'tools/disable-wc-email.php';
require 'tools/trigger-update-callbacks.php';
require 'tools/reset-cys.php';
+require 'tools/set-block-template-logging-threshold.php';
+require 'tools/set-coming-soon-mode.php';
require 'tracks/class-tracks-debug-log.php';
require 'features/features.php';
require 'rest-api-filters/class-wca-test-helper-rest-api-filters.php';
require 'rest-api-filters/hook.php';
require 'live-branches/manifest.php';
require 'live-branches/install.php';
-require 'tools/set-block-template-logging-threshold.php';
require 'remote-spec-validator/class-wca-test-helper-remote-spec-validator.php';
require 'remote-inbox-notifications/class-wca-test-helper-remote-inbox-notifications.php';
diff --git a/plugins/woocommerce-beta-tester/api/tools/set-coming-soon-mode.php b/plugins/woocommerce-beta-tester/api/tools/set-coming-soon-mode.php
new file mode 100644
index 00000000000..00dd6e2884f
--- /dev/null
+++ b/plugins/woocommerce-beta-tester/api/tools/set-coming-soon-mode.php
@@ -0,0 +1,52 @@
+ 'POST',
+ 'args' => array(
+ 'mode' => array(
+ 'description' => 'Coming soon mode',
+ 'type' => 'enum',
+ 'enum' => array( 'site', 'store', 'disabled' ),
+ ),
+ ),
+ )
+);
+
+register_woocommerce_admin_test_helper_rest_route(
+ '/tools/get-force-coming-soon-mode/v1',
+ 'tools_get_coming_soon_mode',
+ array(
+ 'methods' => 'GET',
+ )
+);
+
+/**
+ * A tool to set the coming soon mode.
+ *
+ * @param WP_REST_Request $request Request object.
+ */
+function tools_set_coming_soon_mode( $request ) {
+ $mode = $request->get_param( 'mode' );
+
+ update_option( 'wc_admin_test_helper_force_coming_soon_mode', $mode );
+
+ wc_get_container()->get( ComingSoonCacheInvalidator::class )->invalidate_caches();
+
+ return new WP_REST_Response( $mode, 200 );
+}
+
+/**
+ * A tool to get the coming soon mode.
+ */
+function tools_get_coming_soon_mode() {
+ $mode = get_option( 'wc_admin_test_helper_force_coming_soon_mode', 'disabled' );
+
+ return new WP_REST_Response( $mode, 200 );
+}
diff --git a/plugins/woocommerce-beta-tester/api/tools/trigger-update-callbacks.php b/plugins/woocommerce-beta-tester/api/tools/trigger-update-callbacks.php
index 0a06bbb237c..b13243da012 100644
--- a/plugins/woocommerce-beta-tester/api/tools/trigger-update-callbacks.php
+++ b/plugins/woocommerce-beta-tester/api/tools/trigger-update-callbacks.php
@@ -48,7 +48,7 @@ function trigger_selected_update_callbacks( $request ) {
$update_callbacks = $db_updates[ $version ];
foreach ( $update_callbacks as $update_callback ) {
- call_user_func( $update_callback );
+ \WC_Install::run_update_callback( $update_callback );
}
return false;
diff --git a/plugins/woocommerce-beta-tester/bin/build-zip.sh b/plugins/woocommerce-beta-tester/bin/build-zip.sh
index 76656aced28..55e4505ba43 100755
--- a/plugins/woocommerce-beta-tester/bin/build-zip.sh
+++ b/plugins/woocommerce-beta-tester/bin/build-zip.sh
@@ -10,7 +10,7 @@ rm -rf "$BUILD_PATH"
mkdir -p "$DEST_PATH"
echo "Installing PHP and JS dependencies..."
-pnpm install
+pnpm install --frozen-lockfile
echo "Running JS Build..."
pnpm --filter='@woocommerce/plugin-woocommerce-beta-tester' build || exit "$?"
diff --git a/plugins/woocommerce-beta-tester/changelog/49310-dev-pin-block-env-package b/plugins/woocommerce-beta-tester/changelog/49310-dev-pin-block-env-package
new file mode 100644
index 00000000000..0802125e226
--- /dev/null
+++ b/plugins/woocommerce-beta-tester/changelog/49310-dev-pin-block-env-package
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+Comment: bump wp-env to 9.7.0, include blocks in syncpack
+
diff --git a/plugins/woocommerce-beta-tester/changelog/49983-patch-6 b/plugins/woocommerce-beta-tester/changelog/49983-patch-6
new file mode 100644
index 00000000000..c93a73c169d
--- /dev/null
+++ b/plugins/woocommerce-beta-tester/changelog/49983-patch-6
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Fix "Creation of dynamic property WC_Beta_Tester::$wporg_data is deprecated" on PHP 8.2
\ No newline at end of file
diff --git a/plugins/woocommerce-beta-tester/changelog/add-beta-tester-coming-soon-tool b/plugins/woocommerce-beta-tester/changelog/add-beta-tester-coming-soon-tool
new file mode 100644
index 00000000000..c60a03d3bbf
--- /dev/null
+++ b/plugins/woocommerce-beta-tester/changelog/add-beta-tester-coming-soon-tool
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add tool to force the coming soon landing pages to display on the front-end
diff --git a/plugins/woocommerce-beta-tester/changelog/add-reactify-classic-payments-settings-feature-flag b/plugins/woocommerce-beta-tester/changelog/add-reactify-classic-payments-settings-feature-flag
new file mode 100644
index 00000000000..de54eabf17a
--- /dev/null
+++ b/plugins/woocommerce-beta-tester/changelog/add-reactify-classic-payments-settings-feature-flag
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add reactify-classic-payments-settings feature flag
diff --git a/plugins/woocommerce-beta-tester/changelog/dev-try-faster-building-zip b/plugins/woocommerce-beta-tester/changelog/dev-try-faster-building-zip
new file mode 100644
index 00000000000..310400b9388
--- /dev/null
+++ b/plugins/woocommerce-beta-tester/changelog/dev-try-faster-building-zip
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Monorepo: minor tweaks in zip building script (use frozen lock file when installing dependecies).
diff --git a/plugins/woocommerce-beta-tester/changelog/dev-webpack-loaders-scannig-paths-tweaks b/plugins/woocommerce-beta-tester/changelog/dev-webpack-loaders-scannig-paths-tweaks
new file mode 100644
index 00000000000..30f765e3fca
--- /dev/null
+++ b/plugins/woocommerce-beta-tester/changelog/dev-webpack-loaders-scannig-paths-tweaks
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Monorepo: tweak Webpack loaders paths filtering for better build perfromance.
diff --git a/plugins/woocommerce-beta-tester/changelog/fix-wcadmin-react18-wc-beta-tester b/plugins/woocommerce-beta-tester/changelog/fix-wcadmin-react18-wc-beta-tester
new file mode 100644
index 00000000000..697c3f38fd6
--- /dev/null
+++ b/plugins/woocommerce-beta-tester/changelog/fix-wcadmin-react18-wc-beta-tester
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Changed from using React.render to React.createRoot for wc beta tester as it has been deprecated since React 18
\ No newline at end of file
diff --git a/plugins/woocommerce-beta-tester/changelog/update-qit-false-positive b/plugins/woocommerce-beta-tester/changelog/update-qit-false-positive
new file mode 100644
index 00000000000..4ef13c4c93c
--- /dev/null
+++ b/plugins/woocommerce-beta-tester/changelog/update-qit-false-positive
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Whitelist line with maybe_unserialize() function call from QIT security tests.
diff --git a/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-import-export.php b/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-import-export.php
index f118be302f0..301835f23e8 100644
--- a/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-import-export.php
+++ b/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-import-export.php
@@ -78,7 +78,7 @@ class WC_Beta_Tester_Import_Export {
// show error/update messages.
if ( ! empty( $this->message ) ) {
?>
-
message['type'] ) ? esc_attr( $this->message['type'] ) : '';
?>
@@ -172,6 +172,7 @@ class WC_Beta_Tester_Import_Export {
if ( ! isset( $settings[ $option_name ] ) ) {
continue;
}
+ // nosemgrep scanner.php.wp.security.object-injection, audit.php.wp.security.object-injection
$setting = maybe_unserialize( $settings[ $option_name ] );
if ( is_null( $setting ) ) {
delete_option( $option_name );
diff --git a/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-override-coming-soon-options.php b/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-override-coming-soon-options.php
new file mode 100644
index 00000000000..ebb451da843
--- /dev/null
+++ b/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester-override-coming-soon-options.php
@@ -0,0 +1,77 @@
+wc_beta_tester_override_coming_soon_options();
+ }
+
+ /**
+ * Override the coming soon options.
+ */
+ public function wc_beta_tester_override_coming_soon_options() {
+ $mode = get_option( 'wc_admin_test_helper_force_coming_soon_mode', 'disabled' );
+
+ if ( 'disabled' === $mode ) {
+ return;
+ }
+
+ $is_request_frontend = ( ! is_admin() || defined( 'DOING_AJAX' ) )
+ && ! defined( 'DOING_CRON' ) && ! WC()->is_rest_api_request();
+ if ( ! $is_request_frontend ) {
+ return;
+ }
+
+ $this->override_woocommerce_coming_soon_option( $mode );
+ $this->override_woocommerce_store_pages_only_option( $mode );
+ }
+
+ /**
+ * Override the woocommerce_coming_soon option.
+ *
+ * @param string $mode The coming soon mode.
+ */
+ private function override_woocommerce_coming_soon_option( $mode ) {
+ add_filter(
+ 'option_woocommerce_coming_soon',
+ function ( $value ) use ( $mode ) {
+ if ( 'site' === $mode || 'store' === $mode ) {
+ return 'yes';
+ }
+ return $value;
+ }
+ );
+ }
+
+ /**
+ * Override the woocommerce_store_pages_only option.
+ *
+ * @param string $mode The coming soon mode.
+ */
+ private function override_woocommerce_store_pages_only_option( $mode ) {
+ add_filter(
+ 'option_woocommerce_store_pages_only',
+ function ( $value ) use ( $mode ) {
+ if ( 'store' === $mode ) {
+ return 'yes';
+ }
+ return $value;
+ }
+ );
+ }
+}
+
+new WC_Beta_Tester_Override_Coming_Soon_Options();
diff --git a/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester.php b/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester.php
index fb0668f40f5..fc570dc144a 100644
--- a/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester.php
+++ b/plugins/woocommerce-beta-tester/includes/class-wc-beta-tester.php
@@ -33,6 +33,13 @@ class WC_Beta_Tester {
*/
protected static $instance = null;
+ /**
+ * WP.org data
+ *
+ * @var object
+ */
+ private $wporg_data;
+
/**
* Main Instance.
*/
diff --git a/plugins/woocommerce-beta-tester/package.json b/plugins/woocommerce-beta-tester/package.json
index a33af188e41..aa0072e6cc4 100644
--- a/plugins/woocommerce-beta-tester/package.json
+++ b/plugins/woocommerce-beta-tester/package.json
@@ -17,7 +17,7 @@
"@types/wordpress__plugins": "3.0.0",
"@woocommerce/dependency-extraction-webpack-plugin": "workspace:*",
"@woocommerce/eslint-plugin": "workspace:*",
- "@wordpress/env": "^9.0.7",
+ "@wordpress/env": "^9.7.0",
"@wordpress/prettier-config": "2.17.0",
"@wordpress/scripts": "^19.2.4",
"eslint": "^8.55.0",
diff --git a/plugins/woocommerce-beta-tester/plugin.php b/plugins/woocommerce-beta-tester/plugin.php
index 60ffe6283f3..ec60a8e0856 100644
--- a/plugins/woocommerce-beta-tester/plugin.php
+++ b/plugins/woocommerce-beta-tester/plugin.php
@@ -26,3 +26,4 @@ add_filter( 'woocommerce_admin_get_feature_config', function( $feature_config )
}
return $feature_config;
} );
+
diff --git a/plugins/woocommerce-beta-tester/src/index.js b/plugins/woocommerce-beta-tester/src/index.js
index d56c0ead3ae..ab4fced9c3e 100644
--- a/plugins/woocommerce-beta-tester/src/index.js
+++ b/plugins/woocommerce-beta-tester/src/index.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { render } from '@wordpress/element';
+import { createRoot } from '@wordpress/element';
/**
* Internal dependencies
@@ -16,7 +16,7 @@ const appRoot = document.getElementById(
);
if ( appRoot ) {
- render(
, appRoot );
+ createRoot( appRoot ).render(
);
}
registerProductEditorDevTools();
diff --git a/plugins/woocommerce-beta-tester/src/tools/commands/index.js b/plugins/woocommerce-beta-tester/src/tools/commands/index.js
index 7e131f0d8e4..e077ae389bf 100644
--- a/plugins/woocommerce-beta-tester/src/tools/commands/index.js
+++ b/plugins/woocommerce-beta-tester/src/tools/commands/index.js
@@ -11,6 +11,10 @@ import {
TriggerUpdateCallbacks,
TRIGGER_UPDATE_CALLBACKS_ACTION_NAME,
} from './trigger-update-callbacks';
+import {
+ SetComingSoonMode,
+ UPDATE_COMING_SOON_MODE_ACTION_NAME,
+} from './set-coming-soon-mode';
export default [
{
@@ -78,4 +82,9 @@ export default [
description:
,
action: UPDATE_BLOCK_TEMPLATE_LOGGING_THRESHOLD_ACTION_NAME,
},
+ {
+ command: 'Force coming soon page to show',
+ description:
,
+ action: UPDATE_COMING_SOON_MODE_ACTION_NAME,
+ },
];
diff --git a/plugins/woocommerce-beta-tester/src/tools/commands/set-coming-soon-mode.js b/plugins/woocommerce-beta-tester/src/tools/commands/set-coming-soon-mode.js
new file mode 100644
index 00000000000..69cfab16828
--- /dev/null
+++ b/plugins/woocommerce-beta-tester/src/tools/commands/set-coming-soon-mode.js
@@ -0,0 +1,48 @@
+/**
+ * External dependencies
+ */
+import { SelectControl } from '@wordpress/components';
+import { useDispatch, useSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { STORE_KEY } from '../data/constants';
+
+export const UPDATE_COMING_SOON_MODE_ACTION_NAME = 'updateComingSoonMode';
+
+const OPTIONS = [
+ { label: 'Whole Site', value: 'site' },
+ { label: 'Store Only', value: 'store' },
+ { label: 'Disabled', value: 'disabled' },
+];
+
+export const SetComingSoonMode = () => {
+ const comingSoonMode = useSelect(
+ ( select ) => select( STORE_KEY ).getComingSoonMode(),
+ []
+ );
+ const { updateCommandParams } = useDispatch( STORE_KEY );
+
+ function onChange( mode ) {
+ updateCommandParams( UPDATE_COMING_SOON_MODE_ACTION_NAME, {
+ mode,
+ } );
+ }
+
+ return (
+
+ { ! comingSoonMode ? (
+
Loading ...
+ ) : (
+
+ ) }
+
+ );
+};
diff --git a/plugins/woocommerce-beta-tester/src/tools/commands/trigger-update-callbacks.js b/plugins/woocommerce-beta-tester/src/tools/commands/trigger-update-callbacks.js
index 1c6969fca3c..019a1441b32 100644
--- a/plugins/woocommerce-beta-tester/src/tools/commands/trigger-update-callbacks.js
+++ b/plugins/woocommerce-beta-tester/src/tools/commands/trigger-update-callbacks.js
@@ -3,6 +3,7 @@
*/
import { SelectControl } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
@@ -13,26 +14,33 @@ export const TRIGGER_UPDATE_CALLBACKS_ACTION_NAME =
'runSelectedUpdateCallbacks';
export const TriggerUpdateCallbacks = () => {
- const { dbUpdateVersions } = useSelect( ( select ) => {
- const { getDBUpdateVersions } = select( STORE_KEY );
- return {
- dbUpdateVersions: getDBUpdateVersions(),
- };
- } );
-
+ const dbUpdateVersions = useSelect(
+ ( select ) => select( STORE_KEY ).getDBUpdateVersions(),
+ []
+ );
+ const selectedVersion = useSelect(
+ ( select ) =>
+ select( STORE_KEY ).getCommandParams(
+ TRIGGER_UPDATE_CALLBACKS_ACTION_NAME
+ ).runSelectedUpdateCallbacks.version,
+ []
+ );
const { updateCommandParams } = useDispatch( STORE_KEY );
- function onCronChange( version ) {
+ function onChange( version ) {
updateCommandParams( TRIGGER_UPDATE_CALLBACKS_ACTION_NAME, {
version,
} );
}
- function getOptions() {
- return dbUpdateVersions.map( ( version ) => {
- return { label: version, value: version };
- } );
- }
+ const options = useMemo(
+ () =>
+ dbUpdateVersions.map( ( version ) => ( {
+ label: version,
+ value: version,
+ } ) ),
+ [ dbUpdateVersions ]
+ );
return (
@@ -41,9 +49,10 @@ export const TriggerUpdateCallbacks = () => {
) : (
) }
diff --git a/plugins/woocommerce-beta-tester/src/tools/commands/update-block-template-logging-threshold.tsx b/plugins/woocommerce-beta-tester/src/tools/commands/update-block-template-logging-threshold.tsx
index e17d601adb0..e266051f38c 100644
--- a/plugins/woocommerce-beta-tester/src/tools/commands/update-block-template-logging-threshold.tsx
+++ b/plugins/woocommerce-beta-tester/src/tools/commands/update-block-template-logging-threshold.tsx
@@ -3,7 +3,6 @@
*/
import { SelectControl } from '@wordpress/components';
-import { useEffect, useState } from '@wordpress/element';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore no types
// eslint-disable-next-line @woocommerce/dependency-group
@@ -35,7 +34,6 @@ export const UpdateBlockTemplateLoggingThreshold = () => {
const retrievedLoggingLevels = getLoggingLevels();
const retrievedThreshold = getBlockTemplateLoggingThreshold();
-
return {
loggingLevels: retrievedLoggingLevels,
threshold: retrievedThreshold,
@@ -44,12 +42,9 @@ export const UpdateBlockTemplateLoggingThreshold = () => {
}
);
- const [ newThreshold, setNewThreshold ] = useState( threshold );
-
const { updateCommandParams } = useDispatch( STORE_KEY );
function onThresholdChange( selectedThreshold: string ) {
- setNewThreshold( selectedThreshold );
updateCommandParams(
UPDATE_BLOCK_TEMPLATE_LOGGING_THRESHOLD_ACTION_NAME,
{
@@ -67,10 +62,6 @@ export const UpdateBlockTemplateLoggingThreshold = () => {
} );
}
- useEffect( () => {
- setNewThreshold( threshold );
- }, [ threshold ] );
-
return (
{ isLoading ? (
@@ -83,7 +74,7 @@ export const UpdateBlockTemplateLoggingThreshold = () => {
// @ts-ignore labelPosition prop exists
labelPosition="side"
options={ getOptions() }
- value={ newThreshold }
+ value={ threshold }
/>
) }
diff --git a/plugins/woocommerce-beta-tester/src/tools/data/action-types.js b/plugins/woocommerce-beta-tester/src/tools/data/action-types.js
index b481a90efa1..9cf871b08ba 100644
--- a/plugins/woocommerce-beta-tester/src/tools/data/action-types.js
+++ b/plugins/woocommerce-beta-tester/src/tools/data/action-types.js
@@ -9,10 +9,9 @@ const TYPES = {
IS_EMAIL_DISABLED: 'IS_EMAIL_DISABLED',
SET_DB_UPDATE_VERSIONS: 'SET_DB_UPDATE_VERSIONS',
SET_LOGGING_LEVELS: 'SET_LOGGING_LEVELS',
- SET_BLOCK_TEMPLATE_LOGGING_THRESHOLD:
- 'SET_BLOCK_TEMPLATE_LOGGING_THRESHOLD',
UPDATE_BLOCK_TEMPLATE_LOGGING_THRESHOLD:
'UPDATE_BLOCK_TEMPLATE_LOGGING_THRESHOLD',
+ UPDATE_COMING_SOON_MODE: 'UPDATE_COMING_SOON_MODE',
};
export default TYPES;
diff --git a/plugins/woocommerce-beta-tester/src/tools/data/actions.js b/plugins/woocommerce-beta-tester/src/tools/data/actions.js
index 00d630e3fd6..9e34802dd2c 100644
--- a/plugins/woocommerce-beta-tester/src/tools/data/actions.js
+++ b/plugins/woocommerce-beta-tester/src/tools/data/actions.js
@@ -262,3 +262,13 @@ export function* updateBlockTemplateLoggingThreshold( params ) {
} );
} );
}
+
+export function* updateComingSoonMode( params ) {
+ yield runCommand( 'Update coming soon mode', function* () {
+ yield apiFetch( {
+ path: API_NAMESPACE + '/tools/update-coming-soon-mode/v1',
+ method: 'POST',
+ data: params,
+ } );
+ } );
+}
diff --git a/plugins/woocommerce-beta-tester/src/tools/data/reducer.js b/plugins/woocommerce-beta-tester/src/tools/data/reducer.js
index 7688a308ead..9a15d468c83 100644
--- a/plugins/woocommerce-beta-tester/src/tools/data/reducer.js
+++ b/plugins/woocommerce-beta-tester/src/tools/data/reducer.js
@@ -9,11 +9,14 @@ const DEFAULT_STATE = {
cronJobs: false,
isEmailDisabled: '',
messages: {},
- params: [],
+ params: {
+ updateComingSoonMode: {},
+ updateBlockTemplateLoggingThreshold: {},
+ runSelectedUpdateCallbacks: {},
+ },
status: '',
dbUpdateVersions: [],
loggingLevels: null,
- blockTemplateLoggingThreshold: null,
};
const reducer = ( state = DEFAULT_STATE, action ) => {
@@ -48,7 +51,7 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
return {
...state,
currentlyRunning: {
- ...state,
+ ...state.currentlyRunning,
[ action.command ]: true,
},
};
@@ -56,7 +59,7 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
return {
...state,
currentlyRunning: {
- ...state,
+ ...state.currentlyRunning,
[ action.command ]: false,
},
};
@@ -74,6 +77,7 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
return {
...state,
params: {
+ ...state.params,
[ action.source ]: action.params,
},
};
@@ -87,12 +91,6 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
...state,
loggingLevels: action.loggingLevels,
};
- case TYPES.SET_BLOCK_TEMPLATE_LOGGING_THRESHOLD:
- return {
- ...state,
- blockTemplateLoggingThreshold:
- action.blockTemplateLoggingThreshold,
- };
default:
return state;
}
diff --git a/plugins/woocommerce-beta-tester/src/tools/data/resolvers.js b/plugins/woocommerce-beta-tester/src/tools/data/resolvers.js
index 6409038783b..5f55565925d 100644
--- a/plugins/woocommerce-beta-tester/src/tools/data/resolvers.js
+++ b/plugins/woocommerce-beta-tester/src/tools/data/resolvers.js
@@ -8,12 +8,15 @@ import { apiFetch } from '@wordpress/data-controls';
*/
import { API_NAMESPACE } from './constants';
import {
- setBlockTemplateLoggingThreshold,
setCronJobs,
setDBUpdateVersions,
setIsEmailDisabled,
setLoggingLevels,
+ updateCommandParams,
} from './actions';
+import { UPDATE_BLOCK_TEMPLATE_LOGGING_THRESHOLD_ACTION_NAME } from '../commands/update-block-template-logging-threshold';
+import { UPDATE_COMING_SOON_MODE_ACTION_NAME } from '../commands/set-coming-soon-mode';
+import { TRIGGER_UPDATE_CALLBACKS_ACTION_NAME } from '../commands/trigger-update-callbacks';
export function* getCronJobs() {
const path = `${ API_NAMESPACE }/tools/get-cron-list/v1`;
@@ -33,11 +36,19 @@ export function* getDBUpdateVersions() {
const path = `${ API_NAMESPACE }/tools/get-update-versions/v1`;
try {
- const response = yield apiFetch( {
+ const dbUpdateVersions = yield apiFetch( {
path,
method: 'GET',
} );
- yield setDBUpdateVersions( response );
+
+ dbUpdateVersions.reverse();
+ yield setDBUpdateVersions( dbUpdateVersions );
+ yield updateCommandParams( TRIGGER_UPDATE_CALLBACKS_ACTION_NAME, {
+ version:
+ Array.isArray( dbUpdateVersions ) && dbUpdateVersions.length > 0
+ ? dbUpdateVersions[ 0 ]
+ : null,
+ } );
} catch ( error ) {
throw new Error( error );
}
@@ -76,11 +87,32 @@ export function* getBlockTemplateLoggingThreshold() {
const path = `${ API_NAMESPACE }/tools/get-block-template-logging-threshold/v1`;
try {
- const response = yield apiFetch( {
+ const threshold = yield apiFetch( {
path,
method: 'GET',
} );
- yield setBlockTemplateLoggingThreshold( response );
+ yield updateCommandParams(
+ UPDATE_BLOCK_TEMPLATE_LOGGING_THRESHOLD_ACTION_NAME,
+ {
+ threshold,
+ }
+ );
+ } catch ( error ) {
+ throw new Error( error );
+ }
+}
+
+export function* getComingSoonMode() {
+ const path = `${ API_NAMESPACE }/tools/get-force-coming-soon-mode/v1`;
+
+ try {
+ const mode = yield apiFetch( {
+ path,
+ method: 'GET',
+ } );
+ yield updateCommandParams( UPDATE_COMING_SOON_MODE_ACTION_NAME, {
+ mode: mode || 'disabled',
+ } );
} catch ( error ) {
throw new Error( error );
}
diff --git a/plugins/woocommerce-beta-tester/src/tools/data/selectors.js b/plugins/woocommerce-beta-tester/src/tools/data/selectors.js
index f28fdae371f..1ac46fc713e 100644
--- a/plugins/woocommerce-beta-tester/src/tools/data/selectors.js
+++ b/plugins/woocommerce-beta-tester/src/tools/data/selectors.js
@@ -31,5 +31,9 @@ export function getLoggingLevels( state ) {
}
export function getBlockTemplateLoggingThreshold( state ) {
- return state.blockTemplateLoggingThreshold;
+ return state.params.updateBlockTemplateLoggingThreshold.threshold;
+}
+
+export function getComingSoonMode( state ) {
+ return state.params.updateComingSoonMode.mode;
}
diff --git a/plugins/woocommerce-beta-tester/src/tools/index.js b/plugins/woocommerce-beta-tester/src/tools/index.js
index be33ab55fff..822d03d85b5 100644
--- a/plugins/woocommerce-beta-tester/src/tools/index.js
+++ b/plugins/woocommerce-beta-tester/src/tools/index.js
@@ -77,9 +77,8 @@ function Tools( {
export default compose(
withSelect( ( select ) => {
- const { getCurrentlyRunning, getMessages, getCommandParams } = select(
- STORE_KEY
- );
+ const { getCurrentlyRunning, getMessages, getCommandParams } =
+ select( STORE_KEY );
return {
currentlyRunningCommands: getCurrentlyRunning(),
messages: getMessages(),
diff --git a/plugins/woocommerce-beta-tester/userscripts/wc-live-branches.user.js b/plugins/woocommerce-beta-tester/userscripts/wc-live-branches.user.js
index acf2f83d1d4..c6cbd73ff53 100644
--- a/plugins/woocommerce-beta-tester/userscripts/wc-live-branches.user.js
+++ b/plugins/woocommerce-beta-tester/userscripts/wc-live-branches.user.js
@@ -110,6 +110,7 @@
'launch-your-store',
'minified-js',
'product-custom-fields',
+ 'reactify-classic-payments-settings',
'settings',
];
diff --git a/plugins/woocommerce-beta-tester/webpack.config.js b/plugins/woocommerce-beta-tester/webpack.config.js
index e7fff48497a..76b4f828e0c 100644
--- a/plugins/woocommerce-beta-tester/webpack.config.js
+++ b/plugins/woocommerce-beta-tester/webpack.config.js
@@ -1,5 +1,6 @@
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
const WooCommerceDependencyExtractionWebpackPlugin = require( '@woocommerce/dependency-extraction-webpack-plugin' );
+const path = require( 'path' );
module.exports = {
...defaultConfig,
@@ -15,7 +16,7 @@ module.exports = {
{
test: /\.tsx?$/,
use: 'ts-loader',
- exclude: /node_modules/,
+ include: [ path.resolve( __dirname, './src/' ) ],
},
],
},
diff --git a/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php b/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php
index fbcdcf84f8e..ca0e6b53604 100644
--- a/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php
+++ b/plugins/woocommerce-beta-tester/woocommerce-beta-tester.php
@@ -63,6 +63,7 @@ function _wc_beta_tester_bootstrap() {
new WC_Beta_Tester_Import_Export();
// Tools.
include dirname( __FILE__ ) . '/includes/class-wc-beta-tester-version-picker.php';
+ include dirname( __FILE__ ) . '/includes/class-wc-beta-tester-override-coming-soon-options.php';
register_activation_hook( __FILE__, array( 'WC_Beta_Tester', 'activate' ) );
diff --git a/plugins/woocommerce-blocks/.github/CONTRIBUTING.md b/plugins/woocommerce-blocks/.github/CONTRIBUTING.md
deleted file mode 100644
index ac7d5023b32..00000000000
--- a/plugins/woocommerce-blocks/.github/CONTRIBUTING.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Contributing
-
-Thanks for your interest in contributing to WooCommerce Blocks!
-
-If you wish to contribute code, to get started we recommend first reading our [Getting Started Guide](../docs/contributors/getting-started.md).
-
-All other documentation for contributors can be found [in the docs directory](../docs/README.md).
-
-## Guidelines
-
-Like the WooCommerce project, we want to ensure a welcoming environment for everyone. With that in mind, all contributors are expected to follow our [Code of Conduct](./CODE_OF_CONDUCT.md).
-
-## Reporting Security Issues
-
-Please see [SECURITY.md](./SECURITY.md).
diff --git a/plugins/woocommerce-blocks/.github/comments-aggregator/README.md b/plugins/woocommerce-blocks/.github/comments-aggregator/README.md
deleted file mode 100644
index 667f6eef86f..00000000000
--- a/plugins/woocommerce-blocks/.github/comments-aggregator/README.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# comments-aggregator
-
-> This GitHub Action helps you keep the PR page clean by merging comments/reports by multiple workflows into a single comment.
-
-![screenshot](./screenshot.png)
-
-## Usage
-
-This action is meant to be used as the poster/commenter. Instead of having existing actions post the comment by themselves, set those comments as the action output, then feed that output to `comments-aggregator` to let this action manage those comments for you.
-
-```yml
- - name: Compare Assets
- uses: ./.github/compare-assets
- id: compare-assets
- with:
- repo-token: '${{ secrets.GITHUB_TOKEN }}'
- compare: assets-list/assets.json
- create-comment: false
- - name: Append report
- uses: ./.github/comments-aggregator
- with:
- repo-token: '${{ secrets.GITHUB_TOKEN }}'
- section-id: compare-assets-with-trunk
- content: ${{steps.compare-assets.outputs.comment}}
-```
-
-## Inputs
-
-- **`repo-token`** (required): This is the GitHub token. This is required to manipulate PR comments.
-- **`section-id`** (required): The unique ID that helps this action to update the correct part of the aggregated comment.
-- **`content`** (option): The comment content. Default to empty. If nothing was provided, this action will stop gracefully.
-- **`order`** (optional): The order of the comment part inside the aggregated comment. Default to 10.
-
-## More examples
-
-### Message contains GitHub Event properties
-
-```yml
- - name: Add release ZIP URL as comment to the PR
- uses: ./.github/comments-aggregator
- with:
- repo-token: ${{ secrets.GITHUB_TOKEN }}
- section-id: release-zip-url
- order: 1
- content: |
- The release ZIP for this PR is accessible via:
- ```
- https://wcblocks.wpcomstaging.com/wp-content/uploads/woocommerce-gutenberg-products-block-${{ github.event.pull_request.number }}.zip
- ```
-```
diff --git a/plugins/woocommerce-blocks/.github/comments-aggregator/action.yml b/plugins/woocommerce-blocks/.github/comments-aggregator/action.yml
deleted file mode 100644
index e26a436f1e5..00000000000
--- a/plugins/woocommerce-blocks/.github/comments-aggregator/action.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-name: 'Comments Aggregator'
-description: 'Merge bot comments into one comment to keep PR page clean'
-inputs:
- repo-token:
- description: 'GitHub token'
- required: true
- section-id:
- description: 'Comment section ID for the action to know which part to update'
- required: true
- content:
- description: 'Comment content'
- default: ''
- order:
- description: 'Order of the comment'
- required: false
- default: 10
-runs:
- using: 'node16'
- main: 'index.js'
diff --git a/plugins/woocommerce-blocks/.github/comments-aggregator/index.js b/plugins/woocommerce-blocks/.github/comments-aggregator/index.js
deleted file mode 100644
index 5eb0b925b1e..00000000000
--- a/plugins/woocommerce-blocks/.github/comments-aggregator/index.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * External dependencies
- */
-const { getOctokit, context } = require( '@actions/github' );
-const { setFailed, getInput } = require( '@actions/core' );
-
-/**
- * Internal dependencies
- */
-const { updateComment, isMergedComment } = require( './utils' );
-
-const runner = async () => {
- try {
- const token = getInput( 'repo-token', { required: true } );
- const octokit = getOctokit( token );
- const payload = context.payload;
- const repo = payload.repository.name;
- const owner = payload.repository.owner.login;
-
- // Only run this action on pull requests.
- if ( ! payload.pull_request?.number ) {
- return;
- }
-
- const sectionId = getInput( 'section-id', {
- required: true,
- } );
-
- const content = getInput( 'content' );
- const order = getInput( 'order' );
-
- if ( ! sectionId || ! content ) {
- return;
- }
-
- let commentId, commentBody;
-
- const currentComments = await octokit.rest.issues.listComments( {
- owner,
- repo,
- issue_number: payload.pull_request.number,
- } );
-
- if (
- Array.isArray( currentComments.data ) &&
- currentComments.data.length > 0
- ) {
- const comment = currentComments.data.find( ( comment ) =>
- isMergedComment( comment )
- );
-
- if ( comment ) {
- commentId = comment.id;
- commentBody = comment.body;
- }
- }
-
- commentBody = updateComment( commentBody, {
- sectionId,
- content,
- order,
- } );
-
- if ( commentId ) {
- await octokit.rest.issues.updateComment( {
- owner,
- repo,
- comment_id: commentId,
- body: commentBody,
- } );
- } else {
- await octokit.rest.issues.createComment( {
- owner,
- repo,
- issue_number: payload.pull_request.number,
- body: commentBody,
- } );
- }
- } catch ( error ) {
- setFailed( error.message );
- }
-};
-
-runner();
diff --git a/plugins/woocommerce-blocks/.github/comments-aggregator/screenshot.png b/plugins/woocommerce-blocks/.github/comments-aggregator/screenshot.png
deleted file mode 100644
index db6b666cf88..00000000000
Binary files a/plugins/woocommerce-blocks/.github/comments-aggregator/screenshot.png and /dev/null differ
diff --git a/plugins/woocommerce-blocks/.github/comments-aggregator/utils.js b/plugins/woocommerce-blocks/.github/comments-aggregator/utils.js
deleted file mode 100644
index 341830c90af..00000000000
--- a/plugins/woocommerce-blocks/.github/comments-aggregator/utils.js
+++ /dev/null
@@ -1,89 +0,0 @@
-const identifier = ``;
-const separator = '';
-const footerText =
- '[comments-aggregator](https://github.com/woocommerce/woocommerce-blocks/tree/trunk/.github/comments-aggregator)';
-const footer = `\n>
${ footerText } \n${ identifier }`;
-
-function getSectionId( section ) {
- const match = section.match( /-- section-id: ([^\s]+) --/ );
- return match ? match[ 1 ] : null;
-}
-
-function getSectionOrder( section ) {
- const match = section.match( /-- section-order: ([^\s]+) --/ );
- return match ? match[ 1 ] : null;
-}
-
-function parseComment( comment ) {
- if ( ! comment ) {
- return [];
- }
- const sections = comment.split( separator );
- return sections
- .map( ( section ) => {
- const sectionId = getSectionId( section );
- const order = getSectionOrder( section );
- /**
- * This also remove the footer as it doesn't have a section id. This
- * is intentional as we want the footer to always be the last
- * section.
- */
- if ( ! sectionId ) {
- return null;
- }
- return {
- id: sectionId,
- order: parseInt( order, 10 ),
- content: section.trim(),
- };
- } )
- .filter( Boolean );
-}
-
-function updateSection( sections, data ) {
- const { sectionId, content, order } = data;
- const index = sections.findIndex( ( section ) => section.id === sectionId );
- const formattedContent = `\n\n\n\n${ content }`;
- if ( index === -1 ) {
- sections.push( {
- id: sectionId,
- content: formattedContent,
- } );
- } else {
- sections[ index ].content = formattedContent;
- }
-
- return sections;
-}
-
-function appendFooter( sections ) {
- return sections.concat( {
- id: 'footer',
- content: footer,
- } );
-}
-
-function sortSections( sections ) {
- return sections.sort( ( a, b ) => a.order - b.order );
-}
-
-function combineSections( sections ) {
- return sections
- .map( ( section ) => section.content )
- .join( `\n\n${ separator }\n\n` );
-}
-
-exports.updateComment = function ( comment, data ) {
- let sections = parseComment( comment );
- sections = updateSection( sections, data );
- sections = sortSections( sections );
- sections = appendFooter( sections );
- return combineSections( sections );
-};
-
-exports.isMergedComment = function ( comment ) {
- return (
- comment.body.includes( identifier ) &&
- comment.user.login === 'github-actions[bot]'
- );
-};
diff --git a/plugins/woocommerce-blocks/.github/compare-assets/action.yml b/plugins/woocommerce-blocks/.github/compare-assets/action.yml
deleted file mode 100644
index 704f11f9311..00000000000
--- a/plugins/woocommerce-blocks/.github/compare-assets/action.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-name: 'Compare Assets'
-description: 'Compares two assets files created by DependencyExtractionWebpackPlugin and reports the differences.'
-inputs:
- repo-token:
- description: 'GitHub token'
- required: true
- compare:
- description: 'Path to assets file to compare the build assets with.'
- required: true
- create-comment:
- description: 'Create a dedicate comment for this report?'
- required: false
- default: true
-outputs:
- comment:
- description: 'Markdown comment'
-runs:
- using: 'node16'
- main: 'index.js'
diff --git a/plugins/woocommerce-blocks/.github/compare-assets/index.js b/plugins/woocommerce-blocks/.github/compare-assets/index.js
deleted file mode 100644
index 8876fe18dc1..00000000000
--- a/plugins/woocommerce-blocks/.github/compare-assets/index.js
+++ /dev/null
@@ -1,158 +0,0 @@
-/**
- * External dependencies
- */
-const { getOctokit, context } = require( '@actions/github' );
-const { setFailed, getInput, setOutput } = require( '@actions/core' );
-
-const runner = async () => {
- try {
- const token = getInput( 'repo-token', { required: true } );
- const octokit = getOctokit( token );
- const payload = context.payload;
- const repo = payload.repository.name;
- const owner = payload.repository.owner.login;
- const oldAssets = require( '../../' +
- getInput( 'compare', {
- required: true,
- } ) );
-
- if ( ! oldAssets ) {
- return;
- }
-
- const newAssets = require( '../../../woocommerce/assets/client/blocks/assets.json' );
-
- if ( ! newAssets ) {
- return;
- }
-
- const createComment = getInput( 'create-comment' );
-
- const changes = Object.fromEntries(
- Object.entries( newAssets )
- .map( ( [ key, { dependencies = [] } ] ) => {
- const oldDependencies =
- oldAssets[ key ]?.dependencies || [];
- const added = dependencies.filter(
- ( dependency ) =>
- ! oldDependencies.includes( dependency )
- );
- const removed = oldDependencies.filter(
- ( dependency ) => ! dependencies.includes( dependency )
- );
- return added.length || removed.length
- ? [
- key,
- {
- added,
- removed,
- },
- ]
- : null;
- } )
- .filter( Boolean )
- );
-
- let reportCommentId;
-
- {
- const currentComments = await octokit.rest.issues.listComments( {
- owner,
- repo,
- issue_number: payload.pull_request.number,
- } );
-
- if (
- Array.isArray( currentComments.data ) &&
- currentComments.data.length > 0
- ) {
- const comment = currentComments.data.find(
- ( comment ) =>
- comment.body.includes( 'Script Dependencies Report' ) &&
- comment.user.login === 'github-actions[bot]'
- );
-
- if ( comment ) {
- reportCommentId = comment.id;
- }
- }
- }
-
- let commentBody = '';
-
- if ( Object.keys( changes ).length > 0 ) {
- let reportContent = '';
-
- Object.entries( changes ).forEach(
- ( [ handle, { added, removed } ] ) => {
- const addedDeps = added.length
- ? '`' + added.join( '`, `' ) + '`'
- : '';
- const removedDeps = removed.length
- ? '`' + removed.join( '`, `' ) + '`'
- : '';
-
- let icon = '';
-
- if ( added.length && removed.length ) {
- icon = '❓';
- } else if ( added.length ) {
- icon = '⚠️';
- } else if ( removed.length ) {
- icon = '🎉';
- }
-
- reportContent +=
- `| \`${ handle }\` | ${ addedDeps } | ${ removedDeps } | ${ icon } |` +
- '\n';
- }
- );
-
- commentBody =
- '## Script Dependencies Report' +
- '\n\n' +
- 'The `compare-assets` action has detected some changed script dependencies between this branch and ' +
- 'trunk. Please review and confirm the following are correct before merging.' +
- '\n\n' +
- '| Script Handle | Added | Removed | |' +
- '\n' +
- '| ------------- | ------| ------- | -- |' +
- '\n' +
- reportContent +
- '\n\n' +
- '__This comment was automatically generated by the `./github/compare-assets` action.__';
- } else {
- commentBody =
- '## Script Dependencies Report' +
- '\n\n' +
- 'There is no changed script dependency between this branch and trunk.' +
- '\n\n' +
- '__This comment was automatically generated by the `./github/compare-assets` action.__';
- }
-
- if ( createComment !== 'true' ) {
- setOutput( 'comment', commentBody );
- return;
- }
-
- if ( reportCommentId ) {
- await octokit.rest.issues.updateComment( {
- owner,
- repo,
- comment_id: reportCommentId,
- body: commentBody,
- } );
- } else {
- await octokit.rest.issues.createComment( {
- owner,
- repo,
- issue_number: payload.pull_request.number,
- body: commentBody,
- } );
- }
- } catch ( error ) {
- setFailed( error.message );
- }
-};
-
-runner();
diff --git a/plugins/woocommerce-blocks/.github/config.yml b/plugins/woocommerce-blocks/.github/config.yml
deleted file mode 100644
index dce61b7efde..00000000000
--- a/plugins/woocommerce-blocks/.github/config.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-todo:
- blobLines: 10
- label: false
diff --git a/plugins/woocommerce-blocks/.github/monitor-typescript-errors/action.yml b/plugins/woocommerce-blocks/.github/monitor-typescript-errors/action.yml
deleted file mode 100644
index 6e88f1ace61..00000000000
--- a/plugins/woocommerce-blocks/.github/monitor-typescript-errors/action.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: 'Typescript Monitor'
-description: 'Check TypesScript errors'
-inputs:
- repo-token:
- description: 'GitHub token'
- required: true
- checkstyle:
- description: 'Path checkstyle.xml file of current PR/branch'
- required: true
- checkstyle-trunk:
- description: 'Path checkstyle.xml file of trunk'
- required: true
- create-comment:
- description: 'Create a dedicate comment for this report?'
- required: false
- default: true
-outputs:
- comment:
- description: 'Markdown comment'
-runs:
- using: 'node16'
- main: 'index.js'
diff --git a/plugins/woocommerce-blocks/.github/monitor-typescript-errors/index.js b/plugins/woocommerce-blocks/.github/monitor-typescript-errors/index.js
deleted file mode 100644
index 29544181123..00000000000
--- a/plugins/woocommerce-blocks/.github/monitor-typescript-errors/index.js
+++ /dev/null
@@ -1,73 +0,0 @@
-const fs = require( 'fs' );
-const { getOctokit, context } = require( '@actions/github' );
-const { getInput, setOutput } = require( '@actions/core' );
-const { parseXml, getFilesWithNewErrors } = require( './utils/xml' );
-const { generateMarkdownMessage } = require( './utils/markdown' );
-const { addComment } = require( './utils/github' );
-
-const runner = async () => {
- const token = getInput( 'repo-token', { required: true } );
- const octokit = getOctokit( token );
- const payload = context.payload;
- const repo = payload.repository.name;
- const owner = payload.repository.owner.login;
- const fileName = getInput( 'checkstyle', {
- required: true,
- } );
- const trunkFileName = getInput( 'checkstyle-trunk', {
- required: true,
- } );
- const createComment = getInput( 'create-comment' );
-
- const newCheckStyleFile = fs.readFileSync( fileName );
- const newCheckStyleFileParsed = parseXml( newCheckStyleFile );
- const currentCheckStyleFile = fs.readFileSync( trunkFileName );
- const currentCheckStyleFileContentParsed = parseXml(
- currentCheckStyleFile
- );
-
- const { header } = generateMarkdownMessage( newCheckStyleFileParsed );
- const filesWithNewErrors = getFilesWithNewErrors(
- newCheckStyleFileParsed,
- currentCheckStyleFileContentParsed
- );
-
- const message =
- header +
- '\n' +
- ( filesWithNewErrors.length > 0
- ? `⚠️ ⚠️ This PR introduces new TS errors on ${ filesWithNewErrors.length } files: \n` +
- '
\n' +
- filesWithNewErrors.join( '\n\n' ) +
- '\n' +
- ' '
- : '🎉 🎉 This PR does not introduce new TS errors.' );
-
- if ( process.env[ 'CURRENT_BRANCH' ] !== 'trunk' ) {
- if ( createComment !== 'true' ) {
- setOutput( 'comment', message );
- } else {
- await addComment( {
- octokit,
- owner,
- repo,
- message,
- payload,
- } );
- }
- }
-
- /**
- * @todo: Airtable integration is failing auth, so we're disabling it for now.
- * Issue opened: https://github.com/woocommerce/woocommerce-blocks/issues/8961
- */
- // if ( process.env[ 'CURRENT_BRANCH' ] === 'trunk' ) {
- // try {
- // await addRecord( currentCheckStyleFileContentParsed.totalErrors );
- // } catch ( error ) {
- // setFailed( error );
- // }
- // }
-};
-
-runner();
diff --git a/plugins/woocommerce-blocks/.github/monitor-typescript-errors/utils/airtable.js b/plugins/woocommerce-blocks/.github/monitor-typescript-errors/utils/airtable.js
deleted file mode 100644
index 5d1dcdca607..00000000000
--- a/plugins/woocommerce-blocks/.github/monitor-typescript-errors/utils/airtable.js
+++ /dev/null
@@ -1,38 +0,0 @@
-const axios = require( 'axios' ).default;
-
-const BASE_URL = 'https://api.airtable.com/v0';
-const TABLE_ID = 'appIIlxUVxOks06sZ';
-const API_KEY = process.env[ 'AIRTABLE_API_KEY' ];
-
-const TABLE_NAME = 'TypeScript Migration';
-const TYPESCRIPT_ERRORS_COLUMN_NAME = 'TypeScript Errors';
-const DATE_COLUMN_NAME = 'Date';
-
-// https://community.airtable.com/t/datetime-date-field-woes/32121
-const generateDateValueForAirtable = () => {
- const today = new Date();
- const string = today.toLocaleDateString();
-
- return new Date( string );
-};
-
-exports.addRecord = async ( errorsNumber ) =>
- axios.post(
- `${ BASE_URL }/${ TABLE_ID }/${ TABLE_NAME }`,
- {
- records: [
- {
- fields: {
- [ TYPESCRIPT_ERRORS_COLUMN_NAME ]: errorsNumber,
- [ DATE_COLUMN_NAME ]: generateDateValueForAirtable(),
- },
- },
- ],
- typecast: true,
- },
- {
- headers: {
- Authorization: `Bearer ${ API_KEY }`,
- },
- }
- );
diff --git a/plugins/woocommerce-blocks/.github/monitor-typescript-errors/utils/github.js b/plugins/woocommerce-blocks/.github/monitor-typescript-errors/utils/github.js
deleted file mode 100644
index 2b49d0f3aae..00000000000
--- a/plugins/woocommerce-blocks/.github/monitor-typescript-errors/utils/github.js
+++ /dev/null
@@ -1,44 +0,0 @@
-const getReportCommentId = async ( { octokit, owner, repo, payload } ) => {
- const currentComments = await octokit.rest.issues.listComments( {
- owner,
- repo,
- issue_number: payload.pull_request.number,
- } );
-
- if (
- Array.isArray( currentComments.data ) &&
- currentComments.data.length > 0
- ) {
- const comment = currentComments.data.find(
- ( comment ) =>
- comment.body.includes( 'TypeScript Errors Report' ) &&
- comment.user.login === 'github-actions[bot]'
- );
-
- return comment?.id;
- }
-};
-
-exports.addComment = async ( { octokit, owner, repo, message, payload } ) => {
- const commentId = await getReportCommentId( {
- octokit,
- owner,
- repo,
- payload,
- } );
-
- if ( commentId ) {
- return await octokit.rest.issues.updateComment( {
- owner,
- repo,
- comment_id: commentId,
- body: message,
- } );
- }
- await octokit.rest.issues.createComment( {
- owner,
- repo,
- issue_number: payload.pull_request.number,
- body: message,
- } );
-};
diff --git a/plugins/woocommerce-blocks/.github/monitor-typescript-errors/utils/markdown.js b/plugins/woocommerce-blocks/.github/monitor-typescript-errors/utils/markdown.js
deleted file mode 100644
index 901b0b9701e..00000000000
--- a/plugins/woocommerce-blocks/.github/monitor-typescript-errors/utils/markdown.js
+++ /dev/null
@@ -1,27 +0,0 @@
-exports.generateMarkdownMessage = ( dataFromParsedXml ) => {
- const header = generateHeader( dataFromParsedXml );
- const body = generateBody( dataFromParsedXml );
-
- return { header, body };
-};
-
-const generateHeader = ( dataFromParsedXml ) => {
- return `
-## TypeScript Errors Report
-
-- Files with errors: ${ dataFromParsedXml.totalFilesWithErrors }
-- Total errors: ${ dataFromParsedXml.totalErrors }
-`;
-};
-
-const generateBody = ( dataFromParsedXml ) => {
- const files = dataFromParsedXml.files;
-
- return Object.keys( files ).map( ( file ) => {
- return `
-Files with errors:
- File: ${ file }
- ${ files[ file ].map( ( error ) => `- ${ error }` ).join( '\r\n' ) }
- `;
- } );
-};
diff --git a/plugins/woocommerce-blocks/.github/monitor-typescript-errors/utils/xml.js b/plugins/woocommerce-blocks/.github/monitor-typescript-errors/utils/xml.js
deleted file mode 100644
index 67f99e0dc3e..00000000000
--- a/plugins/woocommerce-blocks/.github/monitor-typescript-errors/utils/xml.js
+++ /dev/null
@@ -1,72 +0,0 @@
-const { XMLParser } = require( 'fast-xml-parser' );
-
-exports.parseXml = ( filePath ) => {
- const parser = new XMLParser( {
- ignoreAttributes: false,
- attributeNamePrefix: '',
- attributesGroupName: '',
- } );
- const parsedFile = parser.parse( filePath );
-
- return getDataFromParsedXml( parsedFile );
-};
-
-const getErrorInfo = ( error ) => {
- const line = error.line;
- const column = error.column;
- const message = error.message;
-
- return {
- line,
- column,
- message,
- };
-};
-
-const getDataFromParsedXml = ( parsedXml ) => {
- const data = parsedXml.checkstyle.file;
-
- return data.reduce(
- ( acc, { name, error } ) => {
- const pathFile = name;
- const hasMultipleErrors = Array.isArray( error );
-
- return {
- files: {
- [ pathFile ]: hasMultipleErrors
- ? error.map( getErrorInfo )
- : [ getErrorInfo( error ) ],
- ...acc.files,
- },
- totalErrors:
- acc.totalErrors + ( hasMultipleErrors ? error.length : 1 ),
- totalFilesWithErrors: acc.totalFilesWithErrors + 1,
- };
- },
- {
- totalErrors: 0,
- totalFilesWithErrors: 0,
- }
- );
-};
-
-exports.getFilesWithNewErrors = (
- newCheckStyleFileParsed,
- currentCheckStyleFileParsed
-) => {
- const newFilesReport = newCheckStyleFileParsed.files;
- const currentFilesReport = currentCheckStyleFileParsed.files;
-
- return Object.keys( newFilesReport )
- .sort( ( a, b ) => a.localeCompare( b ) )
- .reduce(
- ( acc, pathfile ) =>
- typeof currentFilesReport[ pathfile ] === 'undefined' ||
- currentFilesReport[ pathfile ] === null ||
- newFilesReport[ pathfile ].length >
- currentFilesReport[ pathfile ].length
- ? [ ...acc, pathfile ]
- : acc,
- []
- );
-};
diff --git a/plugins/woocommerce-blocks/.github/workflows/auto-merge-dependabot.yml b/plugins/woocommerce-blocks/.github/workflows/auto-merge-dependabot.yml
deleted file mode 100644
index 1f2daa6fa8a..00000000000
--- a/plugins/woocommerce-blocks/.github/workflows/auto-merge-dependabot.yml
+++ /dev/null
@@ -1,66 +0,0 @@
-name: Dependabot auto-merge
-on: pull_request
-
-permissions:
- pull-requests: write
- contents: write
- repository-projects: write
-
-jobs:
- dependabot:
- runs-on: ubuntu-latest
- if: ${{ github.actor == 'dependabot[bot]' }}
- steps:
- - name: Dependabot metadata
- id: metadata
- uses: dependabot/fetch-metadata@v1.6.0
- with:
- github-token: "${{ secrets.GITHUB_TOKEN }}"
-
- - name: Enable auto-merge for Dependabot PRs
- # Automatically merge the following dependency upgrades:
- if: "${{ steps.metadata.outputs.dependency-names == '@actions/core' ||
- steps.metadata.outputs.dependency-names == '@automattic/color-studio' ||
- steps.metadata.outputs.dependency-names == '@babel/cli' ||
- steps.metadata.outputs.dependency-names == '@babel/core' ||
- steps.metadata.outputs.dependency-names == '@babel/plugin-proposal-class-properties' ||
- steps.metadata.outputs.dependency-names == '@babel/plugin-syntax-jsx' ||
- steps.metadata.outputs.dependency-names == '@babel/polyfill' ||
- steps.metadata.outputs.dependency-names == '@types/classnames' ||
- steps.metadata.outputs.dependency-names == '@types/dinero.js' ||
- steps.metadata.outputs.dependency-names == '@types/dompurify' ||
- steps.metadata.outputs.dependency-names == '@types/gtag.js' ||
- steps.metadata.outputs.dependency-names == '@types/jest' ||
- steps.metadata.outputs.dependency-names == '@types/jest-environment-puppeteer' ||
- steps.metadata.outputs.dependency-names == '@types/jquery' ||
- steps.metadata.outputs.dependency-names == '@types/lodash' ||
- steps.metadata.outputs.dependency-names == '@types/puppeteer' ||
- steps.metadata.outputs.dependency-names == '@types/react' ||
- steps.metadata.outputs.dependency-names == '@types/react-dom' ||
- steps.metadata.outputs.dependency-names == '@types/wordpress__block-editor' ||
- steps.metadata.outputs.dependency-names == '@types/wordpress__blocks' ||
- steps.metadata.outputs.dependency-names == '@types/wordpress__data' ||
- steps.metadata.outputs.dependency-names == '@types/wordpress__data-controls' ||
- steps.metadata.outputs.dependency-names == '@types/wordpress__editor' ||
- steps.metadata.outputs.dependency-names == '@types/wordpress__notices' ||
- steps.metadata.outputs.dependency-names == '@typescript-eslint/eslint-plugin' ||
- steps.metadata.outputs.dependency-names == '@typescript-eslint/parser' ||
- steps.metadata.outputs.dependency-names == 'chalk' ||
- steps.metadata.outputs.dependency-names == 'circular-dependency-plugin' ||
- steps.metadata.outputs.dependency-names == 'commander' ||
- steps.metadata.outputs.dependency-names == 'copy-webpack-plugin' ||
- steps.metadata.outputs.dependency-names == 'eslint-import-resolver-typescript' ||
- steps.metadata.outputs.dependency-names == 'gh-pages' ||
- steps.metadata.outputs.dependency-names == 'markdown-it' ||
- steps.metadata.outputs.dependency-names == 'promptly' ||
- steps.metadata.outputs.dependency-names == 'react-docgen' ||
- steps.metadata.outputs.dependency-names == 'wp-types'
- }}"
-
- run: |
- gh pr edit --add-label 'dependencies-auto-merged' "$PR_URL"
- gh pr review --approve "$PR_URL"
- gh pr merge --auto --squash "$PR_URL"
- env:
- PR_URL: ${{github.event.pull_request.html_url}}
- GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
diff --git a/plugins/woocommerce-blocks/.github/workflows/bundle-size.yml b/plugins/woocommerce-blocks/.github/workflows/bundle-size.yml
deleted file mode 100644
index 2b0ac877bce..00000000000
--- a/plugins/woocommerce-blocks/.github/workflows/bundle-size.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-name: Bundle Size
-
-on: [pull_request]
-
-jobs:
- build-and-size:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- with:
- fetch-depth: 1
- - uses: actions/setup-node@v4
- with:
- node-version-file: '.nvmrc'
- - uses: preactjs/compressed-size-action@8a15fc9a36a94c8c3f7835af11a4924da7e95c7c
- with:
- repo-token: '${{ secrets.GITHUB_TOKEN }}'
- pattern: '{build/**/*.js,build/**/*.css}'
diff --git a/plugins/woocommerce-blocks/.github/workflows/check-doc-links-config.json b/plugins/woocommerce-blocks/.github/workflows/check-doc-links-config.json
deleted file mode 100644
index fef225697d5..00000000000
--- a/plugins/woocommerce-blocks/.github/workflows/check-doc-links-config.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "httpHeaders": [
- {
- "urls": [
- "https://github.com/",
- "https://guides.github.com/",
- "https://help.github.com/",
- "https://docs.github.com/"
- ],
- "headers": {
- "Accept-Encoding": "zstd, br, gzip, deflate"
- }
- }
- ],
- "ignorePatterns": [
- {
- "pattern": "^http://localhost"
- },
- {
- "pattern": "https://www.php.net/manual/en/install.php"
- }
- ],
- "retryOn429": true
-}
diff --git a/plugins/woocommerce-blocks/.github/workflows/check-doc-links.yml b/plugins/woocommerce-blocks/.github/workflows/check-doc-links.yml
deleted file mode 100644
index 8a4be5a935a..00000000000
--- a/plugins/woocommerce-blocks/.github/workflows/check-doc-links.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-name: Check Markdown links
-
-on:
- workflow_dispatch:
- pull_request:
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-permissions: {}
-
-jobs:
- markdown_link_check:
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
-
- - name: Install Node.js
- uses: actions/setup-node@v4
- with:
- node-version-file: '.nvmrc'
- cache: 'npm'
-
- - name: Install markdown-link-check
- run: npm install -g markdown-link-check@3.11.2
-
- - name: Run markdown-link-check
- run: |
- find ./docs -path ./docs/internal-developers/testing/releases -prune -o -name "*.md" -print0 | xargs -0 -n1 markdown-link-check -c .github/workflows/check-doc-links-config.json
-
diff --git a/plugins/woocommerce-blocks/.github/workflows/check-modified-assets.yml b/plugins/woocommerce-blocks/.github/workflows/check-modified-assets.yml
deleted file mode 100644
index 723f7dd7def..00000000000
--- a/plugins/woocommerce-blocks/.github/workflows/check-modified-assets.yml
+++ /dev/null
@@ -1,91 +0,0 @@
-name: Check Modified Assets
-
-on:
- pull_request:
-
-jobs:
- build-trunk:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- with:
- ref: trunk
-
- - name: Cache node_modules
- id: cache-node-modules
- uses: actions/cache@v3
- env:
- cache-name: cache-node-modules
- with:
- path: node_modules
- key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }}
- restore-keys: |
- ${{ runner.os }}-build-${{ env.cache-name }}-
- ${{ runner.os }}-build-
- ${{ runner.os }}-
-
- - name: Setup node version and npm cache
- uses: actions/setup-node@v4
- with:
- node-version-file: '.nvmrc'
- cache: 'npm'
-
- - name: Install Node dependencies
- if: steps.cache-node-modules.outputs.cache-hit != 'true'
- run: npm install --no-optional --no-audit
-
- - name: Build Assets
- run: npm run build:check-assets
-
- - name: Upload Artifact
- uses: actions/upload-artifact@v3.1.2
- with:
- name: assets-list
- path: ./build/assets.json
-
- compare-assets-with-trunk:
- needs: [ build-trunk ]
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
-
- - name: Cache node_modules
- id: cache-node-modules
- uses: actions/cache@v3
- env:
- cache-name: cache-node-modules
- with:
- path: node_modules
- key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }}
- restore-keys: |
- ${{ runner.os }}-build-${{ env.cache-name }}-
- ${{ runner.os }}-build-
- ${{ runner.os }}-
-
- - name: Setup node version and npm cache
- uses: actions/setup-node@v4
- with:
- node-version-file: '.nvmrc'
- cache: 'npm'
-
- - name: Build Assets
- run: npm run build:check-assets
-
- - name: Download assets (trunk)
- uses: actions/download-artifact@v3
- with:
- name: assets-list
- path: assets-list
- - name: Compare Assets
- uses: ./.github/compare-assets
- id: compare-assets
- with:
- repo-token: '${{ secrets.GITHUB_TOKEN }}'
- compare: assets-list/assets.json
- create-comment: false
- - name: Append report
- uses: ./.github/comments-aggregator
- with:
- repo-token: '${{ secrets.GITHUB_TOKEN }}'
- section-id: compare-assets-with-trunk
- content: ${{steps.compare-assets.outputs.comment}}
diff --git a/plugins/woocommerce-blocks/.github/workflows/close-stale-issues.yml b/plugins/woocommerce-blocks/.github/workflows/close-stale-issues.yml
deleted file mode 100644
index 5894297538c..00000000000
--- a/plugins/woocommerce-blocks/.github/workflows/close-stale-issues.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-name: 'Close stale issues'
-
-on:
- schedule:
- # Runs daily at 9am UTC
- - cron: '0 9 * * *'
-
-jobs:
- stale:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/stale@v8
- with:
- repo-token: ${{ secrets.GITHUB_TOKEN }}
- days-before-stale: 60
- days-before-pr-stale: 7
- days-before-close: -1
- remove-stale-when-updated: true
- exempt-issue-labels: 'priority: critical,priority: high,Epic,type: technical debt,category: refactor,type: documentation,plugin incompatibility'
- exempt-pr-labels: 'priority: critical,priority: high,Epic,type: technical debt,category: refactor,type: documentation,plugin incompatibility'
- stale-issue-message: "This issue has been marked as `stale` because it has not seen any activity within the past 60 days. Our team uses this tool to help surface issues for review. If you are the author of the issue there's no need to comment as it will be looked at."
- stale-pr-message: "This PR has been marked as `stale` because it has not seen any activity within the past 7 days. Our team uses this tool to help surface pull requests that have slipped through review. \n\n###### If deemed still relevant, the pr can be kept active by ensuring it's up to date with the main branch and removing the stale label."
- stale-issue-label: 'status: stale'
- stale-pr-label: 'status: stale'
diff --git a/plugins/woocommerce-blocks/.github/workflows/codeql-analysis.yml b/plugins/woocommerce-blocks/.github/workflows/codeql-analysis.yml
deleted file mode 100644
index 8c26e53c65e..00000000000
--- a/plugins/woocommerce-blocks/.github/workflows/codeql-analysis.yml
+++ /dev/null
@@ -1,66 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-name: 'CodeQL'
-
-on:
- push:
- branches: [trunk]
- pull_request:
- # The branches below must be a subset of the branches above
- branches: [trunk]
- schedule:
- - cron: '0 16 * * 4'
-
-jobs:
- analyze:
- name: Analyze
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- # Override automatic language detection by changing the below list
- # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
- language: ['javascript']
- # Learn more...
- # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
- with:
- # We must fetch at least the immediate parents so that if this is
- # a pull request then we can checkout the head.
- fetch-depth: 2
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v2
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
- # queries: ./path/to/local/query, your-org/your-repo/queries@main
-
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v2
-
- # ℹ️ Command-line programs to run using the OS shell.
- # 📚 https://git.io/JvXDl
-
- # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
- # and modify them (or add more) to build your code if your project
- # uses a compiled language
-
- #- run: |
- # make bootstrap
- # make release
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
diff --git a/plugins/woocommerce-blocks/.github/workflows/flaky-tests.yml b/plugins/woocommerce-blocks/.github/workflows/flaky-tests.yml
deleted file mode 100644
index 3235c0348b1..00000000000
--- a/plugins/woocommerce-blocks/.github/workflows/flaky-tests.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-name: Report Flaky Tests
-
-on:
- workflow_run:
- workflows: ['E2E tests']
- types:
- - completed
-
-jobs:
- report-to-issues:
- name: Report to GitHub issues
- runs-on: ubuntu-latest
- if: ${{ github.event.workflow_run.conclusion == 'success' }}
- steps:
- # Checkout defaults to using the branch which triggered the event, which
- # isn't necessarily `trunk` (e.g. in the case of a merge).
- - uses: actions/checkout@v3
- with:
- repository: WordPress/gutenberg
- ref: trunk
-
- - name: Use desired version of NodeJS
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
- with:
- node-version-file: '.nvmrc'
- cache: npm
-
- - name: Npm install and build
- # TODO: We don't have to build the entire project, just the action itself.
- run: |
- npm ci
- npm run build:packages
- - name: Report flaky tests
- uses: ./packages/report-flaky-tests
- with:
- repo-token: '${{ secrets.GITHUB_TOKEN }}'
- label: 'type: flaky test'
- artifact-name-prefix: flaky-tests-report
diff --git a/plugins/woocommerce-blocks/.github/workflows/js-css-md-linting.yml b/plugins/woocommerce-blocks/.github/workflows/js-css-md-linting.yml
deleted file mode 100644
index 75ca3655a89..00000000000
--- a/plugins/woocommerce-blocks/.github/workflows/js-css-md-linting.yml
+++ /dev/null
@@ -1,116 +0,0 @@
-name: JavaScript, CSS and Markdown Linting
-
-on:
- pull_request:
- push:
- branches: [trunk]
-permissions:
- actions: write
- checks: write
- pull-requests: read
-
-jobs:
- # cache node and modules
- Setup:
- name: Setup
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
-
- - name: Cache node_modules
- id: cache-node-modules
- uses: actions/cache@v3
- env:
- cache-name: cache-node-modules
- with:
- path: node_modules
- key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
-
- - name: Setup node version and npm cache
- uses: actions/setup-node@v4
- with:
- node-version-file: '.nvmrc'
- cache: 'npm'
-
- - name: Install Node Dependencies
- if: steps.cache-node-modules.outputs.cache-hit != 'true'
- run: npm ci --no-optional
-
- JSLintingCheck:
- name: Lint JavaScript
- needs: Setup
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
-
- - name: Cache node_modules
- id: cache-node-modules
- uses: actions/cache@v3
- env:
- cache-name: cache-node-modules
- with:
- path: node_modules
- key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
-
- - name: Setup node version
- uses: actions/setup-node@v4
- with:
- node-version-file: '.nvmrc'
-
- - name: Save code linting report JSON
- run: npm run lint:js:report
- # Continue to the next step even if this fails
- continue-on-error: true
- - name: Upload ESLint report
- uses: actions/upload-artifact@v3.1.2
- with:
- name: eslint_report.json
- path: eslint_report.json
- - name: Annotate code linting results
- uses: ataylorme/eslint-annotate-action@v2
- with:
- repo-token: '${{ secrets.GITHUB_TOKEN }}'
- report-json: 'eslint_report.json'
-
- CSSLintingCheck:
- name: Lint CSS
- needs: Setup
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - name: Cache node_modules
- id: cache-node-modules
- uses: actions/cache@v3
- env:
- cache-name: cache-node-modules
- with:
- path: node_modules
- key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
-
- - name: Setup node version
- uses: actions/setup-node@v4
- with:
- node-version-file: '.nvmrc'
-
- - name: Lint CSS
- run: npm run lint:css
-
- MDLintingCheck:
- name: Lint MD
- needs: Setup
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - name: Setup node version and npm cache
- uses: actions/setup-node@v4
- with:
- node-version-file: '.nvmrc'
- cache: 'npm'
- - name: Install Node dependencies
- run: npm ci --no-optional
- - name: Lint MD
- run: npm run lint:md:docs
diff --git a/plugins/woocommerce-blocks/.github/workflows/project-management-automations.yml b/plugins/woocommerce-blocks/.github/workflows/project-management-automations.yml
deleted file mode 100644
index b76c2a96a3a..00000000000
--- a/plugins/woocommerce-blocks/.github/workflows/project-management-automations.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-on:
- pull_request:
- types: [opened, synchronize, closed]
- push:
- issues:
- types: [edited]
-name: Project management automations
-permissions:
- pull-requests: write
- actions: write
-jobs:
- project-management-automation:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- with:
- ref: trunk
- - uses: woocommerce/automations@v1
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- automations: todos
diff --git a/plugins/woocommerce-blocks/.github/workflows/scripts/is-community-contributor.js b/plugins/woocommerce-blocks/.github/workflows/scripts/is-community-contributor.js
deleted file mode 100644
index 4ac92674981..00000000000
--- a/plugins/woocommerce-blocks/.github/workflows/scripts/is-community-contributor.js
+++ /dev/null
@@ -1,67 +0,0 @@
-// Note you'll need to install these dependencies as part of your workflow.
-const { Octokit } = require( '@octokit/action' );
-const core = require( '@actions/core' );
-
-// Note that this script assumes you set GITHUB_TOKEN in env, if you don't
-// this won't work.
-const octokit = new Octokit();
-
-const ignoredUsernames = [ 'dependabot[bot]' ];
-const checkIfIgnoredUsername = ( username ) =>
- ignoredUsernames.includes( username );
-
-const getIssueAuthor = ( payload ) => {
- return (
- payload?.issue?.user?.login ||
- payload?.pull_request?.user?.login ||
- null
- );
-};
-
-const isCommunityContributor = async ( owner, repo, username ) => {
- if ( username && ! checkIfIgnoredUsername( username ) ) {
- const {
- data: { permission },
- } = await octokit.rest.repos.getCollaboratorPermissionLevel( {
- owner,
- repo,
- username,
- } );
-
- return permission === 'read' || permission === 'none';
- }
-
- console.log( 'Not a community contributor!' );
- return false;
-};
-
-const addLabel = async ( label, owner, repo, issueNumber ) => {
- await octokit.rest.issues.addLabels( {
- owner,
- repo,
- issue_number: issueNumber,
- labels: [ label ],
- } );
-};
-
-const applyLabelToCommunityContributor = async () => {
- const eventPayload = require( process.env.GITHUB_EVENT_PATH );
- const username = getIssueAuthor( eventPayload );
- const [ owner, repo ] = process.env.GITHUB_REPOSITORY.split( '/' );
- const { number } = eventPayload?.issue || eventPayload?.pull_request;
-
- const isCommunityUser = await isCommunityContributor(
- owner,
- repo,
- username
- );
-
- core.setOutput( 'is-community', isCommunityUser ? 'yes' : 'no' );
-
- if ( isCommunityUser ) {
- console.log( 'Adding community contributor label' );
- await addLabel( 'type: community contribution', owner, repo, number );
- }
-};
-
-applyLabelToCommunityContributor();
diff --git a/plugins/woocommerce-blocks/.github/workflows/typescript-monitoring.yml b/plugins/woocommerce-blocks/.github/workflows/typescript-monitoring.yml
deleted file mode 100644
index 7a997fd4f56..00000000000
--- a/plugins/woocommerce-blocks/.github/workflows/typescript-monitoring.yml
+++ /dev/null
@@ -1,68 +0,0 @@
-name: Monitor TypeScript errors
-on:
- push:
- branches: [trunk]
- pull_request:
-
-jobs:
- check-typescript-errors-with-trunk:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- with:
- ref: 'trunk'
-
- - name: Cache node modules
- uses: actions/cache@v3
- env:
- cache-name: cache-node-modules
- with:
- # npm cache files are stored in `~/.npm` on Linux/macOS
- path: ~/.npm
- key: ${{ runner.OS }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
- restore-keys: |
- ${{ runner.OS }}-build-${{ secrets.CACHE_VERSION }}-${{ env.cache-name }}-
- ${{ runner.OS }}-build-${{ secrets.CACHE_VERSION }}-
-
- - name: Use Node.js
- uses: actions/setup-node@v4
- with:
- node-version-file: '.nvmrc'
- cache: 'npm'
-
- - name: Generate checkstyle for trunk
- run: |
- npm ci
- npm run ts:log-errors
- mv checkstyle.xml $HOME/checkstyle-trunk.xml
-
- - uses: actions/checkout@v3
-
- - name: Generate checkstyle for current PR
- run: |
- npm ci
- npm run ts:log-errors
- mv $HOME/checkstyle-trunk.xml checkstyle-trunk.xml
-
- - name: Get branch name
- id: branch-name
- uses: tj-actions/branch-names@v7
-
- - name: Monitor TypeScript errors
- uses: ./.github/monitor-typescript-errors
- id: monitor-typescript-errors
- with:
- repo-token: '${{ secrets.GITHUB_TOKEN }}'
- checkstyle: checkstyle.xml
- checkstyle-trunk: checkstyle-trunk.xml
- create-comment: false
- env:
- AIRTABLE_API_KEY: ${{ secrets.AIRTABLE_API_KEY }}
- CURRENT_BRANCH: ${{ steps.branch-name.outputs.current_branch }}
- - name: Append report
- uses: ./.github/comments-aggregator
- with:
- repo-token: '${{ secrets.GITHUB_TOKEN }}'
- section-id: monitor-typescript-errors
- content: ${{steps.monitor-typescript-errors.outputs.comment}}
- order: 20
diff --git a/plugins/woocommerce-blocks/.sources/block-previews.sketch b/plugins/woocommerce-blocks/.sources/block-previews.sketch
deleted file mode 100644
index a378fc76f11..00000000000
Binary files a/plugins/woocommerce-blocks/.sources/block-previews.sketch and /dev/null differ
diff --git a/plugins/woocommerce-blocks/.sources/handles.sketch b/plugins/woocommerce-blocks/.sources/handles.sketch
deleted file mode 100644
index 6a7e82ebf2d..00000000000
Binary files a/plugins/woocommerce-blocks/.sources/handles.sketch and /dev/null differ
diff --git a/plugins/woocommerce-blocks/.wordpress-org/banner-1544x500.png b/plugins/woocommerce-blocks/.wordpress-org/banner-1544x500.png
deleted file mode 100644
index 3a83246f164..00000000000
Binary files a/plugins/woocommerce-blocks/.wordpress-org/banner-1544x500.png and /dev/null differ
diff --git a/plugins/woocommerce-blocks/.wordpress-org/banner-772x250.png b/plugins/woocommerce-blocks/.wordpress-org/banner-772x250.png
deleted file mode 100644
index b932f21dd77..00000000000
Binary files a/plugins/woocommerce-blocks/.wordpress-org/banner-772x250.png and /dev/null differ
diff --git a/plugins/woocommerce-blocks/.wordpress-org/icon-128x128.png b/plugins/woocommerce-blocks/.wordpress-org/icon-128x128.png
deleted file mode 100644
index 6d5a0f50715..00000000000
Binary files a/plugins/woocommerce-blocks/.wordpress-org/icon-128x128.png and /dev/null differ
diff --git a/plugins/woocommerce-blocks/.wordpress-org/icon-256x256.png b/plugins/woocommerce-blocks/.wordpress-org/icon-256x256.png
deleted file mode 100644
index 1ff4a1d6106..00000000000
Binary files a/plugins/woocommerce-blocks/.wordpress-org/icon-256x256.png and /dev/null differ
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/price/index.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/price/index.tsx
index 4fce83f49fd..3dd27385f03 100644
--- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/price/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/price/index.tsx
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { registerBlockType } from '@wordpress/blocks';
+import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils';
/**
* Internal dependencies
@@ -28,4 +28,8 @@ const blockConfig = {
edit,
};
-registerBlockType( 'woocommerce/product-price', blockConfig );
+registerBlockSingleProductTemplate( {
+ blockName: 'woocommerce/product-price',
+ blockSettings: blockConfig,
+ isAvailableOnPostEditor: true,
+} );
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/utils/register-block-single-product-template.ts b/plugins/woocommerce-blocks/assets/js/atomic/utils/register-block-single-product-template.ts
index 87784a0011c..58f842c362d 100644
--- a/plugins/woocommerce-blocks/assets/js/atomic/utils/register-block-single-product-template.ts
+++ b/plugins/woocommerce-blocks/assets/js/atomic/utils/register-block-single-product-template.ts
@@ -32,7 +32,7 @@ export const registerBlockSingleProductTemplate = ( {
isAvailableOnPostEditor,
}: {
blockName: string;
- blockMetadata: Partial< BlockConfiguration >;
+ blockMetadata?: string | Partial< BlockConfiguration >;
blockSettings: Partial< BlockConfiguration >;
isAvailableOnPostEditor: boolean;
isVariationBlock?: boolean;
@@ -40,6 +40,10 @@ export const registerBlockSingleProductTemplate = ( {
} ) => {
let currentTemplateId: string | undefined = '';
+ if ( ! blockMetadata ) {
+ blockMetadata = blockName;
+ }
+
subscribe( () => {
const previousTemplateId = currentTemplateId;
const store = select( 'core/edit-site' );
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/utils/render-parent-block.tsx b/plugins/woocommerce-blocks/assets/js/atomic/utils/render-parent-block.tsx
index 913371d0983..24b6c1bd296 100644
--- a/plugins/woocommerce-blocks/assets/js/atomic/utils/render-parent-block.tsx
+++ b/plugins/woocommerce-blocks/assets/js/atomic/utils/render-parent-block.tsx
@@ -15,6 +15,7 @@ import {
hasInnerBlocks,
} from '@woocommerce/blocks-checkout';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
+import type { ReactRootWithContainer } from '@woocommerce/base-utils';
/**
* This file contains logic used on the frontend to convert DOM elements (saved by the block editor) to React
@@ -294,7 +295,7 @@ export const renderParentBlock = ( {
selector: string;
// Function to generate the props object for the block.
getProps: ( el: Element, i: number ) => Record< string, unknown >;
-} ): void => {
+} ): ReactRootWithContainer[] => {
/**
* In addition to getProps, we need to render and return the children. This adds children to props.
*/
@@ -310,7 +311,7 @@ export const renderParentBlock = ( {
/**
* The only difference between using renderParentBlock and renderFrontend is that here we provide children.
*/
- renderFrontend( {
+ return renderFrontend( {
Block,
selector,
getProps: getPropsWithChildren,
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
index 587ea5b1dbc..b6ec1da2a84 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
+++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/cart-line-items-table/cart-line-item-row.tsx
@@ -286,27 +286,27 @@ const CartLineItemRow: React.ForwardRefExoticComponent<
/>
- { ! soldIndividually &&
- !! quantityLimits.editable && (
-
{
- setItemQuantity( newQuantity );
- dispatchStoreEvent(
- 'cart-set-item-quantity',
- {
- product: lineItem,
- quantity: newQuantity,
- }
- );
- } }
- itemName={ name }
- />
- ) }
+ { ! soldIndividually && (
+ {
+ setItemQuantity( newQuantity );
+ dispatchStoreEvent(
+ 'cart-set-item-quantity',
+ {
+ product: lineItem,
+ quantity: newQuantity,
+ }
+ );
+ } }
+ itemName={ name }
+ />
+ ) }
{ showRemoveItemLink && (
( {
field,
@@ -20,10 +21,18 @@ const AddressLine2Field = < T extends AddressFormValues | ContactFormValues >( {
const isFieldRequired = field?.required ?? false;
// Display the input field if it has a value or if it is required.
- const [ isFieldVisible, setFieldVisible ] = useState(
+ const [ isFieldVisible, setIsFieldVisible ] = useState(
() => Boolean( value ) || isFieldRequired
);
+ const handleHiddenInputChange = useCallback(
+ ( newValue: string ) => {
+ onChange( field.key as keyof T, newValue );
+ setIsFieldVisible( true );
+ },
+ [ field.key, onChange ]
+ );
+
return (
{ isFieldVisible ? (
@@ -40,18 +49,33 @@ const AddressLine2Field = < T extends AddressFormValues | ContactFormValues >( {
}
/>
) : (
- setFieldVisible( true ) }
- >
- { sprintf(
- // translators: %s: address 2 field label.
- __( '+ Add %s', 'woocommerce' ),
- field.label.toLowerCase()
- ) }
-
+ <>
+ setIsFieldVisible( true ) }
+ >
+ { sprintf(
+ // translators: %s: address 2 field label.
+ __( '+ Add %s', 'woocommerce' ),
+ field.label.toLowerCase()
+ ) }
+
+
+ handleHiddenInputChange( event.target.value )
+ }
+ />
+ >
) }
);
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/form.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/form.tsx
index 36092522df1..22718c1c2f9 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/form.tsx
+++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/form.tsx
@@ -31,11 +31,16 @@ import { objectHasProp } from '@woocommerce/types';
*/
import { AddressFormProps, AddressFormFields } from './types';
import prepareFormFields from './prepare-form-fields';
-import validateShippingCountry from './validate-shipping-country';
+import validateCountry from './validate-country';
import customValidationHandler from './custom-validation-handler';
-import Combobox from '../../combobox';
import AddressLineFields from './address-line-fields';
-import { createFieldProps, getFieldData } from './utils';
+import {
+ createFieldProps,
+ createCheckboxFieldProps,
+ getFieldData,
+} from './utils';
+import { Select } from '../../select';
+import { validateState } from './validate-state';
/**
* Checkout form.
@@ -48,8 +53,10 @@ const Form = < T extends AddressFormValues | ContactFormValues >( {
addressType = 'shipping',
values,
children,
+ isEditing,
}: AddressFormProps< T > ): JSX.Element => {
const instanceId = useInstanceId( Form );
+ const isFirstRender = useRef( true );
// Track incoming props.
const currentFields = useShallowEqual( fields );
@@ -91,21 +98,63 @@ const Form = < T extends AddressFormValues | ContactFormValues >( {
}
}, [ onChange, addressFormFields, values ] );
- // Maybe validate country when other fields change so user is notified that it's required.
+ // Maybe validate country and state when other fields change so user is notified that they're required.
useEffect( () => {
- if (
- addressType === 'shipping' &&
- objectHasProp( values, 'country' )
- ) {
- validateShippingCountry( values );
+ if ( objectHasProp( values, 'country' ) ) {
+ validateCountry( addressType, values );
}
- }, [ values, addressType ] );
+
+ if ( objectHasProp( values, 'state' ) ) {
+ const stateField = addressFormFields.fields.find(
+ ( f ) => f.key === 'state'
+ );
+
+ if ( stateField ) {
+ validateState( addressType, values, stateField );
+ }
+ }
+ }, [ values, addressType, addressFormFields ] );
// Changing country may change format for postcodes.
useEffect( () => {
fieldsRef.current?.postcode?.revalidate();
}, [ currentCountry ] );
+ // Focus the first input when opening the form.
+ useEffect( () => {
+ let timeoutId: ReturnType< typeof setTimeout >;
+
+ if ( ! isFirstRender.current && isEditing && fieldsRef.current ) {
+ const firstField = addressFormFields.fields.find(
+ ( field ) => field.hidden === false
+ );
+
+ if ( ! firstField ) {
+ return;
+ }
+
+ const { id: firstFieldId } = createFieldProps(
+ firstField,
+ id || `${ instanceId }`,
+ addressType
+ );
+ const firstFieldEl = document.getElementById( firstFieldId );
+
+ if ( firstFieldEl ) {
+ // Focus the first field after a short delay to ensure the form is rendered.
+ timeoutId = setTimeout( () => {
+ firstFieldEl.focus();
+ }, 300 );
+ }
+ }
+
+ isFirstRender.current = false;
+
+ return () => {
+ clearTimeout( timeoutId );
+ };
+ }, [ isEditing, addressFormFields, id, instanceId, addressType ] );
+
id = id || `${ instanceId }`;
return (
@@ -116,6 +165,8 @@ const Form = < T extends AddressFormValues | ContactFormValues >( {
}
const fieldProps = createFieldProps( field, id, addressType );
+ const checkboxFieldProps =
+ createCheckboxFieldProps( fieldProps );
if ( field.key === 'email' ) {
fieldProps.id = 'email';
@@ -133,7 +184,7 @@ const Form = < T extends AddressFormValues | ContactFormValues >( {
[ field.key ]: checked,
} );
} }
- { ...fieldProps }
+ { ...checkboxFieldProps }
/>
);
}
@@ -228,9 +279,10 @@ const Form = < T extends AddressFormValues | ContactFormValues >( {
}
return (
- ( {
} );
} }
options={ field.options }
+ required={ field.required }
+ errorMessage={
+ fieldProps.errorMessage || undefined
+ }
/>
);
}
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/style.scss
new file mode 100644
index 00000000000..fd24db68f17
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/style.scss
@@ -0,0 +1,4 @@
+.wc-block-components-address-form__address_2-hidden-input {
+ position: absolute;
+ left: -20000px;
+}
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/test/address-line-2-field.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/test/address-line-2-field.tsx
new file mode 100644
index 00000000000..4b75195ab8b
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/test/address-line-2-field.tsx
@@ -0,0 +1,42 @@
+/**
+ * External dependencies
+ */
+import { render, screen, fireEvent } from '@testing-library/react';
+import AddressLine2Field from '@woocommerce/base-components/cart-checkout/form/address-line-2-field';
+import { useState } from '@wordpress/element';
+
+describe( 'Address Line 2 Component', () => {
+ it( 'Renders a hidden field which disappears as soon as text is entered (autofill functionality)', async () => {
+ // Render in a wrapper so we can track the value is sent correctly from hidden to visible input.
+ const FieldWrapper = () => {
+ const [ value, setValue ] = useState( '' );
+ return (
+ {
+ setValue( newValue );
+ } }
+ />
+ );
+ };
+ render( );
+ const hiddenInput = screen.getByLabelText( 'Address 2' );
+ expect( hiddenInput ).toBeInTheDocument();
+ expect( hiddenInput ).toHaveAttribute( 'aria-hidden', 'true' );
+ fireEvent.change( hiddenInput, { target: { value: '123' } } );
+ expect( hiddenInput ).not.toBeInTheDocument();
+ const visibleInput = screen.getByLabelText( 'Optional Address 2' );
+ expect( visibleInput ).toBeInTheDocument();
+ expect( visibleInput ).toHaveValue( '123' );
+ } );
+} );
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/test/index.js b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/test/index.js
index 9e420d48410..9151c6e39e7 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/test/index.js
+++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/test/index.js
@@ -21,16 +21,11 @@ jest.mock( '@wordpress/element', () => {
};
} );
-const renderInCheckoutProvider = ( ui, options = { legacyRoot: true } ) => {
+const renderInCheckoutProvider = ( ui, options = {} ) => {
const Wrapper = ( { children } ) => {
return { children } ;
};
const result = render( ui, { wrapper: Wrapper, ...options } );
- // We need to switch to React 17 rendering to allow these tests to keep passing, but as a result the React
- // rendering error will be shown.
- expect( console ).toHaveErroredWith(
- `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot`
- );
return result;
};
@@ -46,7 +41,7 @@ const primaryAddress = {
};
const secondaryAddress = {
country: 'Austria', // We use Austria because it doesn't have states.
- countryKey: 'AU',
+ countryKey: 'AT',
city: 'Vienna',
postcode: 'DCBA',
};
@@ -57,23 +52,29 @@ const tertiaryAddress = {
state: 'Ontario',
postcode: 'EFGH',
};
+const quaternaryAddress = {
+ country: 'Japan',
+ countryKey: 'JP',
+ city: 'Tokyo',
+ postcode: 'IJKL',
+};
-const countryRegExp = /country/i;
const cityRegExp = /city/i;
const stateRegExp = /county|province|state/i;
const postalCodeRegExp = /postal code|postcode|zip/i;
const inputAddress = async ( {
- country = null,
+ countryKey = null,
city = null,
state = null,
postcode = null,
} ) => {
- if ( country ) {
- const countryInput = screen.queryByRole( 'combobox', {
- name: countryRegExp,
- } );
- await userEvent.type( countryInput, country + '{arrowdown}{enter}' );
+ if ( countryKey ) {
+ const countryInput = screen.getByLabelText( 'Country/Region' );
+
+ if ( countryInput ) {
+ await userEvent.selectOptions( countryInput, countryKey );
+ }
}
if ( city ) {
const cityInput = screen.getByLabelText( cityRegExp );
@@ -81,15 +82,12 @@ const inputAddress = async ( {
}
if ( state ) {
- const stateButton = screen.queryByRole( 'combobox', {
- name: stateRegExp,
+ const stateButton = screen.queryByLabelText( stateRegExp, {
+ selector: 'select',
} );
// State input might be a select or a text input.
if ( stateButton ) {
- await userEvent.click( stateButton );
- await userEvent.click(
- screen.getByRole( 'option', { name: state } )
- );
+ await userEvent.selectOptions( stateButton, state );
} else {
const stateInput = screen.getByLabelText( stateRegExp );
await userEvent.type( stateInput, state );
@@ -126,7 +124,7 @@ describe( 'Form Component', () => {
);
};
- it( 'updates context value when interacting with form elements', async () => {
+ test( 'updates context value when interacting with form elements', async () => {
renderInCheckoutProvider(
<>
@@ -152,7 +150,7 @@ describe( 'Form Component', () => {
);
} );
- it( 'input fields update when changing the country', async () => {
+ test( 'input fields update when changing the country', async () => {
renderInCheckoutProvider( );
await act( async () => {
@@ -179,27 +177,19 @@ describe( 'Form Component', () => {
expect( screen.getByLabelText( /Postal code/ ) ).toBeInTheDocument();
} );
- it( 'input values are reset after changing the country', async () => {
+ test( 'input values are reset after changing the country', async () => {
renderInCheckoutProvider( );
+ // First enter an address with no state, but fill the city.
await act( async () => {
await inputAddress( secondaryAddress );
} );
- // Only update `country` to verify other values are reset.
+ // Update country to another country without state.
await act( async () => {
- await inputAddress( { country: primaryAddress.country } );
+ await inputAddress( { countryKey: quaternaryAddress.countryKey } );
} );
- expect( screen.getByLabelText( stateRegExp ).value ).toBe( '' );
-
- // Repeat the test with an address which has a select for the state.
- await act( async () => {
- await inputAddress( tertiaryAddress );
- } );
- await act( async () => {
- await inputAddress( { country: primaryAddress.country } );
- } );
- expect( screen.getByLabelText( stateRegExp ).value ).toBe( '' );
+ expect( screen.getByLabelText( postalCodeRegExp ).value ).toBe( '' );
} );
} );
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/types.ts b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/types.ts
index 291fce9b5cf..921cf032d26 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/types.ts
+++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/types.ts
@@ -37,6 +37,8 @@ export interface AddressFormProps< T > {
values: T;
// support inserting children at end of form
children?: React.ReactNode;
+ // Is the form in editing mode.
+ isEditing?: boolean;
}
interface AddressFieldData {
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/utils.ts b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/utils.ts
index dd2a7227fad..4eaca30a38a 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/utils.ts
+++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/utils.ts
@@ -21,6 +21,7 @@ export interface FieldProps {
autoComplete: string | undefined;
errorMessage: string | undefined;
required: boolean | undefined;
+ placeholder: string | undefined;
className: string;
}
@@ -36,6 +37,7 @@ export const createFieldProps = (
autoComplete: field?.autocomplete,
errorMessage: field?.errorMessage,
required: field?.required,
+ placeholder: field?.placeholder,
className: `wc-block-components-address-form__${ field?.key }`.replaceAll(
'/',
'-'
@@ -43,6 +45,17 @@ export const createFieldProps = (
...field?.attributes,
} );
+export const createCheckboxFieldProps = ( fieldProps: FieldProps ) => {
+ const {
+ errorId,
+ errorMessage,
+ autoCapitalize,
+ autoComplete,
+ placeholder,
+ ...rest
+ } = fieldProps;
+ return rest;
+};
export const getFieldData = < T extends AddressFormValues | ContactFormValues >(
key: 'address_1' | 'address_2',
fields: AddressFormFields[ 'fields' ],
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/validate-shipping-country.ts b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/validate-country.ts
similarity index 85%
rename from plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/validate-shipping-country.ts
rename to plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/validate-country.ts
index 3cbf6bf2db1..3fa7bf32eb5 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/validate-shipping-country.ts
+++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/validate-country.ts
@@ -8,10 +8,14 @@ import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
// If it's the shipping address form and the user starts entering address
// values without having set the country first, show an error.
-const validateShippingCountry = ( values: ShippingAddress ): void => {
- const validationErrorId = 'shipping_country';
+const validateCountry = (
+ addressType: string,
+ values: ShippingAddress
+): void => {
+ const validationErrorId = `${ addressType }_country`;
const hasValidationError =
select( VALIDATION_STORE_KEY ).getValidationError( validationErrorId );
+
if (
! values.country &&
( values.city || values.state || values.postcode )
@@ -37,4 +41,4 @@ const validateShippingCountry = ( values: ShippingAddress ): void => {
}
};
-export default validateShippingCountry;
+export default validateCountry;
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/validate-state.ts b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/validate-state.ts
new file mode 100644
index 00000000000..efceb5fd335
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/validate-state.ts
@@ -0,0 +1,76 @@
+/**
+ * External dependencies
+ */
+import { dispatch, select } from '@wordpress/data';
+import { KeyedFormField, ShippingAddress } from '@woocommerce/settings';
+import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
+import { __, sprintf } from '@wordpress/i18n';
+import isShallowEqual from '@wordpress/is-shallow-equal';
+
+function previousAddress( initialValue?: ShippingAddress ) {
+ let lastValue = initialValue;
+
+ function track( value: ShippingAddress ) {
+ const currentValue = lastValue;
+
+ lastValue = value;
+
+ // Return the previous value
+ return currentValue;
+ }
+
+ return track;
+}
+
+const lastShippingAddress = previousAddress();
+const lastBillingAddress = previousAddress();
+
+export const validateState = (
+ addressType: string,
+ values: ShippingAddress,
+ stateField: KeyedFormField
+) => {
+ const validationErrorId = `${ addressType }_state`;
+ const hasValidationError =
+ select( VALIDATION_STORE_KEY ).getValidationError( validationErrorId );
+ const isRequired = stateField.required;
+
+ const lastAddress =
+ addressType === 'shipping'
+ ? lastShippingAddress( values )
+ : lastBillingAddress( values );
+
+ const addressChanged =
+ !! lastAddress && ! isShallowEqual( lastAddress, values );
+
+ if ( hasValidationError ) {
+ if ( ! isRequired || values.state ) {
+ // Validation error has been set, but it's no longer required, or the state was provided, clear the error.
+ dispatch( VALIDATION_STORE_KEY ).clearValidationError(
+ validationErrorId
+ );
+ } else if ( ! addressChanged ) {
+ // Validation error has been set, there has not been an address change so show the error.
+ dispatch( VALIDATION_STORE_KEY ).showValidationError(
+ validationErrorId
+ );
+ }
+ } else if (
+ ! hasValidationError &&
+ isRequired &&
+ ! values.state &&
+ values.country
+ ) {
+ // No validation has been set yet, if it's required, there is a country set and no state, set the error.
+ dispatch( VALIDATION_STORE_KEY ).setValidationErrors( {
+ [ validationErrorId ]: {
+ message: sprintf(
+ /* translators: %s will be the state field label in lowercase e.g. "state" */
+ __( 'Please select a %s', 'woocommerce' ),
+ stateField.label.toLowerCase()
+ ),
+ hidden: true,
+ },
+ } );
+ }
+};
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/shipping-calculator/address.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/shipping-calculator/address.tsx
index 50c596a2803..d97eb1a78d3 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/shipping-calculator/address.tsx
+++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/shipping-calculator/address.tsx
@@ -73,7 +73,13 @@ const ShippingCalculatorAddress = ( {
const isAddressValid = validateSubmit();
if ( isAddressValid ) {
- return onUpdate( address );
+ const addressToSubmit = {};
+ addressFields.forEach( ( key ) => {
+ if ( typeof address[ key ] !== 'undefined' ) {
+ addressToSubmit[ key ] = address[ key ];
+ }
+ } );
+ return onUpdate( addressToSubmit );
}
} }
type="submit"
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/shipping-rates-control-package/package-rates.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/shipping-rates-control-package/package-rates.tsx
index 23914fbb066..8584d7e9523 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/shipping-rates-control-package/package-rates.tsx
+++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/shipping-rates-control-package/package-rates.tsx
@@ -38,15 +38,9 @@ const PackageRates = ( {
const previousSelectedRateId = usePrevious( selectedRateId );
// Store selected rate ID in local state so shipping rates changes are shown in the UI instantly.
- const [ selectedOption, setSelectedOption ] = useState( () => {
- if ( selectedRateId ) {
- return selectedRateId;
- }
- // Default to first rate if no rate is selected.
- if ( rates.length > 0 ) {
- return rates[ 0 ].rate_id;
- }
- } );
+ const [ selectedOption, setSelectedOption ] = useState(
+ selectedRateId ?? ''
+ );
// Update the selected option if cart state changes in the data store.
useEffect( () => {
@@ -61,7 +55,6 @@ const PackageRates = ( {
// Update the selected option if there is no rate selected on mount.
useEffect( () => {
- // The useState callback run only once, so we need this to update it right fetching new rates.
if ( ! selectedOption && rates.length > 0 ) {
setSelectedOption( rates[ 0 ].rate_id );
onSelectRate( rates[ 0 ].rate_id );
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/combobox/index.tsx b/plugins/woocommerce-blocks/assets/js/base/components/combobox/index.tsx
index f7ea9f75eda..eab707f80ff 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/combobox/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/base/components/combobox/index.tsx
@@ -3,13 +3,12 @@
*/
import clsx from 'clsx';
import { __ } from '@wordpress/i18n';
-import { useEffect, useId, useRef, useState } from '@wordpress/element';
+import { useEffect, useId, useRef } from '@wordpress/element';
import { ComboboxControl } from 'wordpress-components';
import { ValidationInputError } from '@woocommerce/blocks-components';
import { isObject } from '@woocommerce/types';
import { useDispatch, useSelect } from '@wordpress/data';
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
-import { Icon, chevronUp, chevronDown } from '@wordpress/icons';
/**
* Internal dependencies
@@ -66,8 +65,6 @@ const Combobox = ( {
};
} );
- const [ isFocused, setIsFocused ] = useState( false );
-
useEffect( () => {
if ( ! required || value ) {
clearValidationError( errorId );
@@ -101,17 +98,11 @@ const Combobox = ( {
'has-error': error?.message && ! error?.hidden,
} ) }
ref={ controlRef }
- onFocus={ () => setIsFocused( true ) }
- onBlur={ () => setIsFocused( false ) }
>
{
- onChange( selectedValue );
- setIsFocused( false );
- } }
- onSelect={ () => setIsFocused( false ) }
+ onChange={ onChange }
onFilterValueChange={ ( filterValue: string ) => {
if ( filterValue.length ) {
// If we have a value and the combobox is not focussed, this could be from browser autofill.
@@ -163,7 +154,6 @@ const Combobox = ( {
aria-invalid={ error?.message && ! error?.hidden }
aria-errormessage={ validationErrorId }
/>
-
);
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/combobox/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/combobox/style.scss
index 584b1878b37..e6218493bb0 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/combobox/style.scss
+++ b/plugins/woocommerce-blocks/assets/js/base/components/combobox/style.scss
@@ -6,16 +6,6 @@
.wc-block-components-form .wc-block-components-combobox,
.wc-block-components-combobox {
- position: relative;
-
- svg {
- fill: currentColor;
- position: absolute;
- right: 15px;
- top: 15px;
- pointer-events: none;
- }
-
.wc-block-components-combobox-control {
@include reset-typography();
@include reset-box();
@@ -41,10 +31,10 @@
line-height: em($gap);
box-sizing: border-box;
outline: inherit;
- border: 1px solid $universal-border-light;
+ border: 1px solid $input-border-gray;
background: #fff;
box-shadow: none;
- color: currentColor;
+ color: $input-text-active;
font-family: inherit;
font-weight: normal;
letter-spacing: inherit;
@@ -55,14 +45,14 @@
width: 100%;
opacity: initial;
border-radius: $universal-border-radius;
- max-height: 50px;
+ height: 50px;
&[aria-expanded="true"],
&:focus {
background-color: #fff;
+ color: $input-text-active;
outline: 0;
- box-shadow: 0 0 0 2px currentColor;
- border-bottom: 0;
+ box-shadow: 0 0 0 1px $input-border-gray;
}
&[aria-expanded="true"] {
@@ -78,7 +68,7 @@
&:focus {
background-color: $input-background-dark;
color: $input-text-dark;
- box-shadow: 0 0 0 2px $input-border-gray;
+ box-shadow: 0 0 0 1px $input-border-dark;
}
}
}
@@ -86,19 +76,18 @@
position: absolute;
z-index: 10;
background-color: $select-dropdown-light;
- border: 2px solid currentColor;
+ border: 1px solid $input-border-gray;
border-top: 0;
- border-bottom-width: 2px;
+ border-bottom: 0;
margin: 3em 0 0 0;
padding: 0;
max-height: 300px;
- min-width: calc(100% + 4px);
- left: -2px;
- top: -1px;
+ min-width: 100%;
overflow: auto;
color: currentColor;
border-bottom-left-radius: $universal-border-radius;
border-bottom-right-radius: $universal-border-radius;
+ box-shadow: 0 1px 0 1px $input-border-gray;
box-sizing: border-box;
.has-dark-controls & {
@@ -108,7 +97,7 @@
.components-form-token-field__suggestion {
@include font-size(regular);
- color: currentColor;
+ color: $gray-700;
cursor: default;
list-style: none;
margin: 0;
@@ -144,10 +133,10 @@
transform: translateY(em($gap));
line-height: 1.25; // =20px when font-size is 16px.
left: em($gap-smaller);
- top: 0;
+ top: -2px;
transform-origin: top left;
transition: all 200ms ease;
- color: currentColor;
+ color: $universal-body-low-emphasis;
z-index: 1;
margin: 0;
overflow: hidden;
@@ -166,7 +155,7 @@
.wc-block-components-combobox-control:has(input:-webkit-autofill) {
label {
- transform: translateY(25%) scale(0.75);
+ transform: translateY(#{$gap-smallest}) scale(0.75);
}
}
@@ -174,7 +163,7 @@
&:focus-within {
.wc-block-components-combobox-control
label.components-base-control__label {
- transform: translateY(25%) scale(0.75);
+ transform: translateY(#{$gap-smallest}) scale(0.75);
}
}
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/country-input/billing-country-input.tsx b/plugins/woocommerce-blocks/assets/js/base/components/country-input/billing-country-input.tsx
index 82e1efe4d5f..9aa6152abd7 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/country-input/billing-country-input.tsx
+++ b/plugins/woocommerce-blocks/assets/js/base/components/country-input/billing-country-input.tsx
@@ -10,7 +10,9 @@ import CountryInput from './country-input';
import type { CountryInputProps } from './CountryInputProps';
const BillingCountryInput = ( props: CountryInputProps ): JSX.Element => {
- return
;
+ const { ...restOfProps } = props;
+
+ return
;
};
export default BillingCountryInput;
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/country-input/country-input.tsx b/plugins/woocommerce-blocks/assets/js/base/components/country-input/country-input.tsx
index 30091aa33ec..b61a5157835 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/country-input/country-input.tsx
+++ b/plugins/woocommerce-blocks/assets/js/base/components/country-input/country-input.tsx
@@ -2,16 +2,15 @@
* External dependencies
*/
import { useMemo } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
import clsx from 'clsx';
/**
* Internal dependencies
*/
-import Combobox from '../combobox';
import './style.scss';
import type { CountryInputWithCountriesProps } from './CountryInputProps';
+import { Select, SelectOption } from '../select';
export const CountryInput = ( {
className,
@@ -22,36 +21,27 @@ export const CountryInput = ( {
value = '',
autoComplete = 'off',
required = false,
- errorId,
- errorMessage = __( 'Please select a country', 'woocommerce' ),
}: CountryInputWithCountriesProps ): JSX.Element => {
- const options = useMemo(
- () =>
- Object.entries( countries ).map(
- ( [ countryCode, countryName ] ) => ( {
- value: countryCode,
- label: decodeEntities( countryName ),
- } )
- ),
- [ countries ]
- );
+ const options = useMemo< SelectOption[] >( () => {
+ return Object.entries( countries ).map(
+ ( [ countryCode, countryName ] ) => ( {
+ value: countryCode,
+ label: decodeEntities( countryName ),
+ } )
+ );
+ }, [ countries ] );
return (
-
-
-
+ id={ id }
+ label={ label || '' }
+ onChange={ onChange }
+ options={ options }
+ value={ value }
+ required={ required }
+ autoComplete={ autoComplete }
+ />
);
};
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/index.ts b/plugins/woocommerce-blocks/assets/js/base/components/index.ts
index ac18ea408aa..971ca329e58 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/index.ts
+++ b/plugins/woocommerce-blocks/assets/js/base/components/index.ts
@@ -23,6 +23,7 @@ export * from './product-rating';
export * from './quantity-selector';
export * from './read-more';
export * from './reviews';
+export * from './select';
export * from './sidebar-layout';
export * from './snackbar-list';
export * from './state-input';
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/product-rating/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/product-rating/style.scss
index 888c0548297..94e0bdaa63d 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/product-rating/style.scss
+++ b/plugins/woocommerce-blocks/assets/js/base/components/product-rating/style.scss
@@ -88,14 +88,15 @@ $line-height: 1.618;
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font-family: WooCommerce;
font-weight: 400;
- -webkit-text-stroke: 2px var(--wp--preset--color--black, #000);
&::before {
content: "\53";
top: 0;
left: 0;
right: 0;
position: absolute;
- color: transparent;
+ -webkit-text-stroke-color: inherit;
+ -webkit-text-stroke-width: 2px;
+ -webkit-text-fill-color: transparent;
white-space: nowrap;
text-align: center;
}
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/quantity-selector/index.tsx b/plugins/woocommerce-blocks/assets/js/base/components/quantity-selector/index.tsx
index 5c06589dc54..ee83e688741 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/quantity-selector/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/base/components/quantity-selector/index.tsx
@@ -47,9 +47,13 @@ export interface QuantitySelectorProps {
*/
itemName?: string;
/**
- * Whether the component should be interactable or not
+ * Whether the component should be interactable
*/
disabled: boolean;
+ /**
+ * Whether the component should be editable
+ */
+ editable: boolean;
}
const QuantitySelector = ( {
@@ -61,6 +65,7 @@ const QuantitySelector = ( {
step = 1,
itemName = '',
disabled,
+ editable,
}: QuantitySelectorProps ): JSX.Element => {
const classes = clsx( 'wc-block-components-quantity-selector', className );
@@ -158,6 +163,7 @@ const QuantitySelector = ( {
ref={ inputRef }
className="wc-block-components-quantity-selector__input"
disabled={ disabled }
+ readOnly={ ! editable }
type="number"
step={ step }
min={ minimum }
@@ -184,54 +190,64 @@ const QuantitySelector = ( {
itemName
) }
/>
-
{
- const newQuantity = quantity - step;
- onChange( newQuantity );
- speak(
- sprintf(
- /* translators: %s refers to the item's new quantity in the cart. */
- __( 'Quantity reduced to %s.', 'woocommerce' ),
- newQuantity
- )
- );
- normalizeQuantity( newQuantity );
- } }
- >
- -
-
-
{
- const newQuantity = quantity + step;
- onChange( newQuantity );
- speak(
- sprintf(
- /* translators: %s refers to the item's new quantity in the cart. */
- __( 'Quantity increased to %s.', 'woocommerce' ),
- newQuantity
- )
- );
- normalizeQuantity( newQuantity );
- } }
- >
- +
-
+ { editable && (
+ <>
+
{
+ const newQuantity = quantity - step;
+ onChange( newQuantity );
+ speak(
+ sprintf(
+ /* translators: %s refers to the item's new quantity in the cart. */
+ __(
+ 'Quantity reduced to %s.',
+ 'woocommerce'
+ ),
+ newQuantity
+ )
+ );
+ normalizeQuantity( newQuantity );
+ } }
+ >
+ -
+
+
{
+ const newQuantity = quantity + step;
+ onChange( newQuantity );
+ speak(
+ sprintf(
+ /* translators: %s refers to the item's new quantity in the cart. */
+ __(
+ 'Quantity increased to %s.',
+ 'woocommerce'
+ ),
+ newQuantity
+ )
+ );
+ normalizeQuantity( newQuantity );
+ } }
+ >
+ +
+
+ >
+ ) }
);
};
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/quantity-selector/test/index.tsx b/plugins/woocommerce-blocks/assets/js/base/components/quantity-selector/test/index.tsx
new file mode 100644
index 00000000000..ecfee6f9f83
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/base/components/quantity-selector/test/index.tsx
@@ -0,0 +1,45 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import QuantitySelector, { QuantitySelectorProps } from '../index';
+
+const defaults = {
+ disabled: false,
+ editable: true,
+ itemName: 'product',
+ maximum: 9999,
+ onChange: () => void 0,
+} as QuantitySelectorProps;
+
+describe( 'QuantitySelector', () => {
+ it( 'The quantity step buttons are rendered when the quantity is editable', () => {
+ const { rerender } = render(
);
+
+ expect(
+ screen.getByLabelText(
+ `Increase quantity of ${ defaults.itemName }`
+ )
+ ).toBeInTheDocument();
+ expect(
+ screen.getByLabelText( `Reduce quantity of ${ defaults.itemName }` )
+ ).toBeInTheDocument();
+
+ rerender(
);
+
+ expect(
+ screen.queryByLabelText(
+ `Increase quantity of ${ defaults.itemName }`
+ )
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByLabelText(
+ `Reduce quantity of ${ defaults.itemName }`
+ )
+ ).not.toBeInTheDocument();
+ } );
+} );
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/select/index.tsx b/plugins/woocommerce-blocks/assets/js/base/components/select/index.tsx
new file mode 100644
index 00000000000..561ed12592b
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/base/components/select/index.tsx
@@ -0,0 +1,176 @@
+/**
+ * External dependencies
+ */
+import { Icon, chevronDown } from '@wordpress/icons';
+import { useCallback, useId, useMemo, useEffect } from '@wordpress/element';
+import { sprintf, __ } from '@wordpress/i18n';
+import { useSelect, useDispatch } from '@wordpress/data';
+import clsx from 'clsx';
+import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
+import { ValidationInputError } from '@woocommerce/blocks-components';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+
+export type SelectOption = {
+ value: string;
+ label: string;
+ disabled?: boolean;
+};
+
+type SelectProps = Omit<
+ React.SelectHTMLAttributes< HTMLSelectElement >,
+ 'onChange'
+> & {
+ options: SelectOption[];
+ label: string;
+ onChange: ( newVal: string ) => void;
+ errorId?: string;
+ required?: boolean;
+ errorMessage?: string;
+};
+
+export const Select = ( props: SelectProps ) => {
+ const {
+ onChange,
+ options,
+ label,
+ value = '',
+ className,
+ size,
+ errorId: incomingErrorId,
+ required,
+ errorMessage = __( 'Please select a valid option', 'woocommerce' ),
+ placeholder,
+ ...restOfProps
+ } = props;
+ const selectOnChange = useCallback(
+ ( event: React.ChangeEvent< HTMLSelectElement > ) => {
+ onChange( event.target.value );
+ },
+ [ onChange ]
+ );
+
+ const emptyOption: SelectOption = useMemo(
+ () => ( {
+ value: '',
+ label:
+ placeholder ??
+ sprintf(
+ // translators: %s will be label of the field. For example "country/region".
+ __( 'Select a %s', 'woocommerce' ),
+ label?.toLowerCase()
+ ),
+ disabled: !! required,
+ } ),
+ [ label, placeholder, required ]
+ );
+
+ const generatedId = useId();
+ const inputId =
+ restOfProps.id || `wc-blocks-components-select-${ generatedId }`;
+ const errorId = incomingErrorId || inputId;
+
+ const optionsWithEmpty = useMemo< SelectOption[] >( () => {
+ if ( required && value ) {
+ return options;
+ }
+ return [ emptyOption ].concat( options );
+ }, [ required, value, emptyOption, options ] );
+
+ const { setValidationErrors, clearValidationError } =
+ useDispatch( VALIDATION_STORE_KEY );
+
+ const { error, validationErrorId } = useSelect( ( select ) => {
+ const store = select( VALIDATION_STORE_KEY );
+ return {
+ error: store.getValidationError( errorId ),
+ validationErrorId: store.getValidationErrorId( errorId ),
+ };
+ } );
+
+ useEffect( () => {
+ if ( ! required || value ) {
+ clearValidationError( errorId );
+ } else {
+ setValidationErrors( {
+ [ errorId ]: {
+ message: errorMessage,
+ hidden: true,
+ },
+ } );
+ }
+ return () => {
+ clearValidationError( errorId );
+ };
+ }, [
+ clearValidationError,
+ value,
+ errorId,
+ errorMessage,
+ required,
+ setValidationErrors,
+ ] );
+
+ const validationError = useSelect( ( select ) => {
+ const store = select( VALIDATION_STORE_KEY );
+ return (
+ store.getValidationError( errorId || '' ) || {
+ hidden: true,
+ }
+ );
+ } );
+
+ return (
+
+
+
+
+ { label }
+
+
+ { optionsWithEmpty.map( ( option ) => (
+
+ { option.label }
+
+ ) ) }
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/select/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/select/style.scss
new file mode 100644
index 00000000000..ac375550c48
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/base/components/select/style.scss
@@ -0,0 +1,104 @@
+.wc-blocks-components-select {
+ width: 100%;
+
+ .wc-blocks-components-select__container {
+ border-radius: $universal-border-radius;
+ box-sizing: border-box;
+ border: 1px solid $universal-border-strong;
+ background: #fff;
+ width: 100%;
+ height: 50px;
+ position: relative;
+
+ .has-dark-controls & {
+ background-color: $input-background-dark;
+ border-color: $input-border-dark;
+ color: $input-text-dark;
+
+ &:focus {
+ background-color: $input-background-dark;
+ color: $input-text-dark;
+ box-shadow: 0 0 0 2px $input-border-gray;
+ }
+ }
+
+ .has-error & {
+ border-color: $alert-red;
+ }
+ }
+
+ .wc-blocks-components-select__select {
+ @include reset-typography();
+ @include font-size(regular);
+ border-radius: $universal-border-radius;
+ border: none;
+ width: 100%;
+ height: 100%;
+ appearance: none;
+ background: none;
+ padding: em($gap) em($gap-smaller) 0;
+ color: $input-text-active;
+
+ &:focus {
+ outline: 0;
+ box-shadow: 0 0 0 1px inherit;
+ }
+
+ .has-dark-controls & {
+ color: $input-text-dark;
+ }
+
+ .has-error & {
+ color: $alert-red;
+ }
+ }
+
+ .wc-blocks-components-select__label {
+ @include reset-typography();
+ @include font-size(regular);
+ position: absolute;
+ line-height: 1.25; // =20px when font-size is 16px.
+ left: em($gap-smaller);
+ top: 2px;
+ transform-origin: top left;
+ transition: all 200ms ease;
+ color: $input-text-active;
+ z-index: 1;
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: calc(100% - #{2 * $gap});
+ white-space: nowrap;
+
+ .has-dark-controls & {
+ color: $input-placeholder-dark;
+ }
+
+ .has-error & {
+ color: $alert-red;
+ }
+
+ @media screen and (prefers-reduced-motion: reduce) {
+ transition: none;
+ }
+
+ transform: translateY(15%) scale(0.75);
+ }
+
+ .wc-blocks-components-select__expand {
+ position: absolute;
+ transform: translate(0%, -50%);
+ top: 50%;
+ right: $gap-small;
+ pointer-events: none;
+ fill: $input-text-active;
+
+ .has-dark-controls & {
+ fill: $input-text-dark;
+ }
+
+ .has-error & {
+ fill: $alert-red;
+ }
+ }
+}
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/state-input/billing-state-input.tsx b/plugins/woocommerce-blocks/assets/js/base/components/state-input/billing-state-input.tsx
index 847c71d2039..e9a12b0a607 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/state-input/billing-state-input.tsx
+++ b/plugins/woocommerce-blocks/assets/js/base/components/state-input/billing-state-input.tsx
@@ -10,7 +10,9 @@ import StateInput from './state-input';
import type { StateInputProps } from './StateInputProps';
const BillingStateInput = ( props: StateInputProps ): JSX.Element => {
- return
;
+ const { ...restOfProps } = props;
+
+ return
;
};
export default BillingStateInput;
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx b/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx
index a569716cd80..9099ebe6e1b 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx
+++ b/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx
@@ -1,18 +1,17 @@
/**
* External dependencies
*/
-import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
import { useCallback, useMemo, useEffect, useRef } from '@wordpress/element';
-import clsx from 'clsx';
import { ValidatedTextInput } from '@woocommerce/blocks-components';
+import { clsx } from 'clsx';
/**
* Internal dependencies
*/
-import Combobox from '../combobox';
import './style.scss';
import type { StateInputWithStatesProps } from './StateInputProps';
+import { Select, SelectOption } from '../select';
const optionMatcher = (
value: string,
@@ -36,19 +35,17 @@ const StateInput = ( {
autoComplete = 'off',
value = '',
required = false,
- errorId = '',
}: StateInputWithStatesProps ): JSX.Element => {
const countryStates = states[ country ];
- const options = useMemo(
- () =>
- countryStates
- ? Object.keys( countryStates ).map( ( key ) => ( {
- value: key,
- label: decodeEntities( countryStates[ key ] ),
- } ) )
- : [],
- [ countryStates ]
- );
+ const options = useMemo< SelectOption[] >( () => {
+ if ( countryStates && Object.keys( countryStates ).length > 0 ) {
+ return Object.keys( countryStates ).map( ( key ) => ( {
+ value: key,
+ label: decodeEntities( countryStates[ key ] ),
+ } ) );
+ }
+ return [];
+ }, [ countryStates ] );
/**
* Handles state selection onChange events. Finds a matching state by key or value.
@@ -91,20 +88,18 @@ const StateInput = ( {
if ( options.length > 0 ) {
return (
-
);
}
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/state-input/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/state-input/style.scss
index 47288567e82..5ef6f3c0339 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/state-input/style.scss
+++ b/plugins/woocommerce-blocks/assets/js/base/components/state-input/style.scss
@@ -1,3 +1,3 @@
-.wc-block-components-state-input {
+.wc-blocks-components-select__container {
margin-top: $gap;
}
diff --git a/plugins/woocommerce-blocks/assets/js/base/context/hooks/use-show-shipping-total-warning.ts b/plugins/woocommerce-blocks/assets/js/base/context/hooks/use-show-shipping-total-warning.ts
index 0260f51d391..69d892a66cb 100644
--- a/plugins/woocommerce-blocks/assets/js/base/context/hooks/use-show-shipping-total-warning.ts
+++ b/plugins/woocommerce-blocks/assets/js/base/context/hooks/use-show-shipping-total-warning.ts
@@ -6,8 +6,6 @@ import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { hasShippingRate } from '@woocommerce/base-components/cart-checkout/totals/shipping/utils';
-import { hasCollectableRate } from '@woocommerce/base-utils';
-import { isString } from '@woocommerce/types';
/**
* Internal dependencies
@@ -18,64 +16,36 @@ export const useShowShippingTotalWarning = () => {
const context = 'woocommerce/checkout-totals-block';
const errorNoticeId = 'wc-blocks-totals-shipping-warning';
- const { shippingRates } = useShippingData();
+ const { shippingRates, hasSelectedLocalPickup } = useShippingData();
const hasRates = hasShippingRate( shippingRates );
- const {
- prefersCollection,
- isRateBeingSelected,
- shippingNotices,
- cartData,
- } = useSelect( ( select ) => {
- return {
- cartData: select( CART_STORE_KEY ).getCartData(),
- prefersCollection: select( CHECKOUT_STORE_KEY ).prefersCollection(),
- isRateBeingSelected:
- select( CART_STORE_KEY ).isShippingRateBeingSelected(),
- shippingNotices: select( 'core/notices' ).getNotices( context ),
- };
- } );
+ const { prefersCollection, isRateBeingSelected, shippingNotices } =
+ useSelect( ( select ) => {
+ return {
+ prefersCollection:
+ select( CHECKOUT_STORE_KEY ).prefersCollection(),
+ isRateBeingSelected:
+ select( CART_STORE_KEY ).isShippingRateBeingSelected(),
+ shippingNotices: select( 'core/notices' ).getNotices( context ),
+ };
+ } );
const { createInfoNotice, removeNotice } = useDispatch( 'core/notices' );
useEffect( () => {
+ const isShowingNotice = shippingNotices.length > 0;
+ const hasMismatch = ! prefersCollection && hasSelectedLocalPickup;
+
if ( ! hasRates || isRateBeingSelected ) {
// Early return because shipping rates were not yet loaded from the cart data store, or the user is changing
// rate, no need to alter the notice until we know what the actual rate is.
+ if ( isShowingNotice ) {
+ // Removes the notice in case it was already shown.
+ removeNotice( errorNoticeId, context );
+ }
return;
}
- const selectedRates = cartData?.shippingRates?.reduce(
- ( acc: string[], rate ) => {
- const selectedRateForPackage = rate.shipping_rates.find(
- ( shippingRate ) => {
- return shippingRate.selected;
- }
- );
- if (
- typeof selectedRateForPackage?.method_id !== 'undefined'
- ) {
- acc.push( selectedRateForPackage?.method_id );
- }
- return acc;
- },
- []
- );
- const isPickupRateSelected = Object.values( selectedRates ).some(
- ( rate: unknown ) => {
- if ( isString( rate ) ) {
- return hasCollectableRate( rate );
- }
- return false;
- }
- );
-
// There is a mismatch between the method the user chose (pickup or shipping) and the currently selected rate.
- if (
- hasRates &&
- ! prefersCollection &&
- ! isRateBeingSelected &&
- isPickupRateSelected &&
- shippingNotices.length === 0
- ) {
+ if ( hasMismatch && ! isShowingNotice ) {
createInfoNotice(
__(
'Totals will be recalculated when a valid shipping method is selected.',
@@ -91,14 +61,11 @@ export const useShowShippingTotalWarning = () => {
}
// Don't show the notice if they have selected local pickup, or if they have selected a valid regular shipping rate.
- if (
- ( prefersCollection || ! isPickupRateSelected ) &&
- shippingNotices.length > 0
- ) {
+ if ( ! hasMismatch && isShowingNotice ) {
removeNotice( errorNoticeId, context );
}
}, [
- cartData?.shippingRates,
+ hasSelectedLocalPickup,
createInfoNotice,
hasRates,
isRateBeingSelected,
diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/checkout-processor.ts b/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/checkout-processor.ts
index 04dd423d580..60035cb4002 100644
--- a/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/checkout-processor.ts
+++ b/plugins/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/checkout-processor.ts
@@ -50,29 +50,31 @@ const CheckoutProcessor = () => {
const { onCheckoutValidation } = useCheckoutEventsContext();
const {
+ additionalFields,
+ customerId,
+ customerPassword,
+ extensionData,
hasError: checkoutHasError,
- redirectUrl,
- isProcessing: checkoutIsProcessing,
isBeforeProcessing: checkoutIsBeforeProcessing,
isComplete: checkoutIsComplete,
+ isProcessing: checkoutIsProcessing,
orderNotes,
+ redirectUrl,
shouldCreateAccount,
- extensionData,
- customerId,
- additionalFields,
} = useSelect( ( select ) => {
const store = select( CHECKOUT_STORE_KEY );
return {
+ additionalFields: store.getAdditionalFields(),
+ customerId: store.getCustomerId(),
+ customerPassword: store.getCustomerPassword(),
+ extensionData: store.getExtensionData(),
hasError: store.hasError(),
- redirectUrl: store.getRedirectUrl(),
- isProcessing: store.isProcessing(),
isBeforeProcessing: store.isBeforeProcessing(),
isComplete: store.isComplete(),
+ isProcessing: store.isProcessing(),
orderNotes: store.getOrderNotes(),
+ redirectUrl: store.getRedirectUrl(),
shouldCreateAccount: store.getShouldCreateAccount(),
- extensionData: store.getExtensionData(),
- customerId: store.getCustomerId(),
- additionalFields: store.getAdditionalFields(),
};
} );
@@ -248,17 +250,18 @@ const CheckoutProcessor = () => {
: {};
const data = {
- shipping_address: cartNeedsShipping
- ? emptyHiddenAddressFields( currentShippingAddress.current )
- : undefined,
+ additional_fields: additionalFields,
billing_address: emptyHiddenAddressFields(
currentBillingAddress.current
),
- additional_fields: additionalFields,
- customer_note: orderNotes,
create_account: shouldCreateAccount,
- ...paymentData,
+ customer_note: orderNotes,
+ customer_password: customerPassword,
extensions: { ...extensionData },
+ shipping_address: cartNeedsShipping
+ ? emptyHiddenAddressFields( currentShippingAddress.current )
+ : undefined,
+ ...paymentData,
};
triggerFetch( {
@@ -327,6 +330,8 @@ const CheckoutProcessor = () => {
activePaymentMethod,
orderNotes,
shouldCreateAccount,
+ customerId,
+ customerPassword,
extensionData,
additionalFields,
cartNeedsShipping,
diff --git a/plugins/woocommerce-blocks/assets/js/base/hooks/use-observed-viewport.ts b/plugins/woocommerce-blocks/assets/js/base/hooks/use-observed-viewport.ts
index 1ecb186fd7f..88444d139f7 100644
--- a/plugins/woocommerce-blocks/assets/js/base/hooks/use-observed-viewport.ts
+++ b/plugins/woocommerce-blocks/assets/js/base/hooks/use-observed-viewport.ts
@@ -43,10 +43,22 @@ export function useObservedViewport< T extends HTMLElement >(): [
const resizeObserver = new ResizeObserver( ( entries ) => {
entries.forEach( ( entry ) => {
if ( entry.target === element ) {
+ let elementTop = '0';
+
+ if ( element.computedStyleMap ) {
+ elementTop =
+ element
+ .computedStyleMap()
+ .get( 'top' )
+ ?.toString() || elementTop;
+ } else {
+ // Firefox support
+ elementTop =
+ getComputedStyle( element ).top || elementTop;
+ }
+
const { height, width } = entry.contentRect;
- const elementTop =
- element.computedStyleMap().get( 'top' )?.toString() ||
- '0';
+
setObservedElement( {
height: height + parseInt( elementTop, 10 ),
width,
diff --git a/plugins/woocommerce-blocks/assets/js/base/utils/render-frontend.tsx b/plugins/woocommerce-blocks/assets/js/base/utils/render-frontend.tsx
index 4867fd9815f..0a1739a5407 100644
--- a/plugins/woocommerce-blocks/assets/js/base/utils/render-frontend.tsx
+++ b/plugins/woocommerce-blocks/assets/js/base/utils/render-frontend.tsx
@@ -1,8 +1,9 @@
/**
* External dependencies
*/
-import { render, Suspense } from '@wordpress/element';
+import { createRoot, useEffect, Suspense } from '@wordpress/element';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
+import type { Root } from 'react-dom/client';
// Some blocks take care of rendering their inner blocks automatically. For
// example, the empty cart. In those cases, we don't want to trigger the render
@@ -27,6 +28,11 @@ export type GetPropsFn<
TAttributes extends Record< string, unknown >
> = ( el: HTMLElement, i: number ) => BlockProps< TProps, TAttributes >;
+export type ReactRootWithContainer = {
+ container: HTMLElement;
+ root: Root;
+};
+
interface RenderBlockParams<
TProps extends Record< string, unknown >,
TAttributes extends Record< string, unknown >
@@ -55,20 +61,32 @@ export const renderBlock = <
attributes = {} as TAttributes,
props = {} as BlockProps< TProps, TAttributes >,
errorBoundaryProps = {},
-}: RenderBlockParams< TProps, TAttributes > ): void => {
- render(
-
- }>
- { Block && }
-
- ,
- container,
- () => {
+}: RenderBlockParams< TProps, TAttributes > ): Root => {
+ const BlockWrapper = () => {
+ useEffect( () => {
if ( container.classList ) {
container.classList.remove( 'is-loading' );
}
- }
- );
+ }, [] );
+
+ return (
+
+ Loading...
+ }
+ >
+ { Block && (
+
+ ) }
+
+
+ );
+ };
+
+ const root = createRoot( container );
+ root.render( );
+ return root;
};
interface RenderBlockInContainersParams<
@@ -99,10 +117,14 @@ const renderBlockInContainers = <
containers,
getProps = () => ( {} as BlockProps< TProps, TAttributes > ),
getErrorBoundaryProps = () => ( {} ),
-}: RenderBlockInContainersParams< TProps, TAttributes > ): void => {
+}: RenderBlockInContainersParams<
+ TProps,
+ TAttributes
+> ): ReactRootWithContainer[] => {
if ( containers.length === 0 ) {
- return;
+ return [];
}
+ const roots: ReactRootWithContainer[] = [];
// Use Array.forEach for IE11 compatibility.
Array.prototype.forEach.call( containers, ( el, i ) => {
@@ -114,14 +136,19 @@ const renderBlockInContainers = <
...( props.attributes || {} ),
};
- renderBlock( {
- Block,
+ roots.push( {
container: el,
- props,
- attributes,
- errorBoundaryProps,
+ root: renderBlock( {
+ Block,
+ container: el,
+ props,
+ attributes,
+ errorBoundaryProps,
+ } ),
} );
} );
+
+ return roots;
};
// Given an element and a list of wrappers, check if the element is inside at
@@ -157,7 +184,10 @@ const renderBlockOutsideWrappers = <
getErrorBoundaryProps,
selector,
wrappers,
-}: RenderBlockOutsideWrappersParams< TProps, TAttributes > ): void => {
+}: RenderBlockOutsideWrappersParams<
+ TProps,
+ TAttributes
+> ): ReactRootWithContainer[] => {
const containers = document.body.querySelectorAll( selector );
// Filter out blocks inside the wrappers.
if ( wrappers && wrappers.length > 0 ) {
@@ -165,7 +195,8 @@ const renderBlockOutsideWrappers = <
return ! isElementInsideWrappers( el, wrappers );
} );
}
- renderBlockInContainers( {
+
+ return renderBlockInContainers( {
Block,
containers,
getProps,
@@ -234,20 +265,21 @@ export const renderFrontend = <
props:
| RenderBlockOutsideWrappersParams< TProps, TAttributes >
| RenderBlockInsideWrapperParams< TProps, TAttributes >
-): void => {
+): ReactRootWithContainer[] => {
const wrappersToSkipOnLoad = document.body.querySelectorAll(
selectorsToSkipOnLoad.join( ',' )
);
const { Block, getProps, getErrorBoundaryProps, selector } = props;
- renderBlockOutsideWrappers( {
+ const roots = renderBlockOutsideWrappers( {
Block,
getProps,
getErrorBoundaryProps,
selector,
wrappers: wrappersToSkipOnLoad,
} );
+
// For each wrapper, add an event listener to render the inner blocks when
// `wc-blocks_render_blocks_frontend` event is triggered.
Array.prototype.forEach.call( wrappersToSkipOnLoad, ( wrapper ) => {
@@ -255,6 +287,8 @@ export const renderFrontend = <
renderBlockInsideWrapper( { ...props, wrapper } );
} );
} );
+
+ return roots;
};
export default renderFrontend;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/attribute-filter/test/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/attribute-filter/test/block.tsx
index ecdc9b111ae..3b1b191e7ff 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/attribute-filter/test/block.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/attribute-filter/test/block.tsx
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { render, screen } from '@testing-library/react';
+import { act, render, screen } from '@testing-library/react';
import * as hooks from '@woocommerce/base-context/hooks';
import userEvent from '@testing-library/user-event';
@@ -106,14 +106,8 @@ const setup = ( params: SetupParams ) => {
results: stubCollectionData(),
isLoading: false,
} );
- const utils = render( , {
- legacyRoot: true,
- } );
- // We need to switch to React 17 rendering to allow these tests to keep passing, but as a result the React
- // rendering error will be shown.
- expect( console ).toHaveErroredWith(
- `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot`
- );
+ const utils = render( );
+
const applyButton = screen.getByRole( 'button', { name: /apply/i } );
const smallAttributeCheckbox = screen.getByRole( 'checkbox', {
name: /small/i,
@@ -164,8 +158,10 @@ describe( 'Filter by Attribute block', () => {
test( 'should enable Apply button when filter attributes are changed', async () => {
const { applyButton, smallAttributeCheckbox } =
setupWithoutSelectedFilterAttributes();
- await userEvent.click( smallAttributeCheckbox );
+ await act( async () => {
+ await userEvent.click( smallAttributeCheckbox );
+ } );
expect( applyButton ).not.toBeDisabled();
} );
} );
@@ -180,18 +176,25 @@ describe( 'Filter by Attribute block', () => {
test( 'should enable Apply button when filter attributes are changed', async () => {
const { applyButton, smallAttributeCheckbox } =
setupWithSelectedFilterAttributes();
- await userEvent.click( smallAttributeCheckbox );
+ await act( async () => {
+ await userEvent.click( smallAttributeCheckbox );
+ } );
expect( applyButton ).not.toBeDisabled();
} );
test( 'should disable Apply button when deselecting the same previously selected attribute', async () => {
const { applyButton, smallAttributeCheckbox } =
setupWithSelectedFilterAttributes( { filterSize: 'small' } );
- await userEvent.click( smallAttributeCheckbox );
+
+ await act( async () => {
+ await userEvent.click( smallAttributeCheckbox );
+ } );
expect( applyButton ).not.toBeDisabled();
- await userEvent.click( smallAttributeCheckbox );
+ await act( async () => {
+ await userEvent.click( smallAttributeCheckbox );
+ } );
expect( applyButton ).toBeDisabled();
} );
} );
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss
index fcc8196bd68..29078a50348 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss
+++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss
@@ -30,7 +30,7 @@ $border-width: 1px;
.wc-block-components-express-payment__event-buttons {
list-style: none;
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(calc(33% - 10px), 1fr));
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
grid-gap: 10px;
@include breakpoint("<782px") {
@@ -111,7 +111,7 @@ $border-width: 1px;
align-items: center;
text-align: center;
padding: 0 $gap-large;
- margin: $gap-larger 0 ( 3.5 * $grid-unit );
+ margin: $gap-larger 0 (3.5 * $grid-unit);
&::before {
margin-right: 10px;
@@ -142,7 +142,8 @@ $border-width: 1px;
// For Twenty Twenty we need to increase specificity of the title.
.theme-twentytwenty {
- .wc-block-components-express-payment .wc-block-components-express-payment__title {
+ .wc-block-components-express-payment
+ .wc-block-components-express-payment__title {
padding-left: $gap-small;
padding-right: $gap-small;
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/index.tsx
index 944a28e5440..c6b97fe1e51 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/index.tsx
@@ -22,11 +22,13 @@ const AddressCard = ( {
onEdit,
target,
fieldConfig,
+ isExpanded,
}: {
address: CartShippingAddress | CartBillingAddress;
onEdit: () => void;
target: string;
fieldConfig: FormFieldsConfig;
+ isExpanded: boolean;
} ): JSX.Element | null => {
const countryData = getSetting< Record< string, CountryData > >(
'countryData',
@@ -50,6 +52,10 @@ const AddressCard = ( {
address,
formatToUse
);
+ const label =
+ target === 'shipping'
+ ? __( 'Edit shipping address', 'woocommerce' )
+ : __( 'Edit billing address', 'woocommerce' );
return (
);
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/style.scss
index 5bbae0e698d..2d2715b45c9 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/style.scss
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/address-card/style.scss
@@ -35,7 +35,15 @@
}
}
.wc-block-components-address-card__edit {
+ background-color: transparent;
+ border: 0;
+ color: inherit;
+ cursor: pointer;
+ font-family: inherit;
margin: 0 0 0 auto;
- text-decoration: none;
@include font-size(small);
+
+ &:hover {
+ text-decoration: underline;
+ }
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/block.tsx
index 905bf6af4ad..c82136aee2c 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/block.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/block.tsx
@@ -139,7 +139,8 @@ const ScrollOnError = ( {
// Scroll after a short timeout to allow a re-render. This will allow focusableSelector to match updated components.
scrollToTopTimeout = window.setTimeout( () => {
scrollToTop( {
- focusableSelector: 'input:invalid, .has-error input',
+ focusableSelector:
+ 'input:invalid, .has-error input, .has-error select',
} );
}, 50 );
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx
index c59bc46022c..bcdcbac3e69 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/block.tsx
@@ -7,6 +7,7 @@ import {
useCheckoutAddress,
useEditorContext,
noticeContexts,
+ useShippingData,
} from '@woocommerce/base-context';
import Noninteractive from '@woocommerce/base-components/noninteractive';
import type { ShippingAddress, FormFieldsConfig } from '@woocommerce/settings';
@@ -42,6 +43,7 @@ const Block = ( {
useBillingAsShipping,
} = useCheckoutAddress();
const { isEditor } = useEditorContext();
+ const { needsShipping } = useShippingData();
// Syncs shipping address with billing address if "Force shipping to the customer billing address" is enabled.
useEffectOnce( () => {
@@ -110,7 +112,7 @@ const Block = ( {
shippingAddress
);
const defaultEditingAddress =
- isEditor || ! hasAddress || billingMatchesShipping;
+ isEditor || ! hasAddress || ( needsShipping && billingMatchesShipping );
return (
<>
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx
index cc48f9351d5..b36708f4314 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx
@@ -83,9 +83,10 @@ const CustomerAddress = ( {
setEditing( true );
} }
fieldConfig={ addressFieldsConfig }
+ isExpanded={ editing }
/>
),
- [ billingAddress, addressFieldsConfig ]
+ [ billingAddress, addressFieldsConfig, editing ]
);
const renderAddressFormComponent = useCallback(
@@ -98,10 +99,11 @@ const CustomerAddress = ( {
values={ billingAddress }
fields={ ADDRESS_FORM_KEYS }
fieldConfig={ addressFieldsConfig }
+ isEditing={ editing }
/>
>
),
- [ addressFieldsConfig, billingAddress, onChangeAddress ]
+ [ addressFieldsConfig, billingAddress, onChangeAddress, editing ]
);
return (
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/block.tsx
index ae2cb1a679b..7bd211e3415 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/block.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/block.tsx
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { __ } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
import {
useCheckoutAddress,
useStoreEvents,
@@ -17,48 +17,90 @@ import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import { CONTACT_FORM_KEYS } from '@woocommerce/block-settings';
import { Form } from '@woocommerce/base-components/cart-checkout';
-const Block = (): JSX.Element => {
- const { customerId, shouldCreateAccount, additionalFields } = useSelect(
- ( select ) => {
- const store = select( CHECKOUT_STORE_KEY );
- return {
- customerId: store.getCustomerId(),
- shouldCreateAccount: store.getShouldCreateAccount(),
- additionalFields: store.getAdditionalFields(),
- };
- }
- );
+/**
+ * Internal dependencies
+ */
+import CreatePassword from './create-password';
- const { __internalSetShouldCreateAccount, setAdditionalFields } =
+const CreateAccountUI = (): React.ReactElement | null => {
+ const { shouldCreateAccount } = useSelect( ( select ) => {
+ const store = select( CHECKOUT_STORE_KEY );
+ return {
+ shouldCreateAccount: store.getShouldCreateAccount(),
+ };
+ } );
+ const { __internalSetShouldCreateAccount, __internalSetCustomerPassword } =
useDispatch( CHECKOUT_STORE_KEY );
+
+ // Work out what fields need to be displayed for the current shopper.
+ const allowGuestCheckout = getSetting( 'checkoutAllowsGuest', false );
+ const allowSignup = getSetting( 'checkoutAllowsSignup', false );
+ const generatePassword = getSetting( 'generatePassword', false );
+ const showCreateAccountCheckbox = allowGuestCheckout && allowSignup;
+ const showCreateAccountPassword = generatePassword
+ ? false
+ : ( showCreateAccountCheckbox && shouldCreateAccount ) ||
+ ! allowGuestCheckout;
+
+ if (
+ ! allowGuestCheckout &&
+ ! showCreateAccountCheckbox &&
+ ! showCreateAccountPassword
+ ) {
+ return null;
+ }
+
+ return (
+ <>
+ { allowGuestCheckout && (
+
+ { __(
+ 'You are currently checking out as a guest.',
+ 'woocommerce'
+ ) }
+
+ ) }
+ { showCreateAccountCheckbox && (
+ {
+ __internalSetShouldCreateAccount( value );
+ __internalSetCustomerPassword( '' );
+ } }
+ />
+ ) }
+ { showCreateAccountPassword && }
+ >
+ );
+};
+
+const Block = (): JSX.Element => {
+ const { additionalFields, customerId } = useSelect( ( select ) => {
+ const store = select( CHECKOUT_STORE_KEY );
+ return {
+ additionalFields: store.getAdditionalFields(),
+ customerId: store.getCustomerId(),
+ };
+ } );
+
+ const { setAdditionalFields } = useDispatch( CHECKOUT_STORE_KEY );
const { billingAddress, setEmail } = useCheckoutAddress();
const { dispatchCheckoutEvent } = useStoreEvents();
-
const onChangeEmail = ( value: string ) => {
setEmail( value );
dispatchCheckoutEvent( 'set-email-address' );
};
-
- const createAccountVisible =
- ! customerId &&
- getSetting( 'checkoutAllowsGuest', false ) &&
- getSetting( 'checkoutAllowsSignup', false );
-
- const createAccountUI = createAccountVisible && (
- __internalSetShouldCreateAccount( value ) }
- />
- );
-
const onChangeForm = ( newAddress: ContactFormValues ) => {
const { email, ...additionalValues } = newAddress;
onChangeEmail( email );
setAdditionalFields( additionalValues );
};
-
const contactFormValues = {
email: billingAddress.email,
...additionalFields,
@@ -76,7 +118,7 @@ const Block = (): JSX.Element => {
values={ contactFormValues }
fields={ CONTACT_FORM_KEYS }
>
- { createAccountUI }
+ { ! customerId && }
>
);
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/create-password.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/create-password.tsx
new file mode 100644
index 00000000000..68e4b2b85df
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/create-password.tsx
@@ -0,0 +1,68 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useState } from '@wordpress/element';
+import { ValidatedTextInput } from '@woocommerce/blocks-components';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
+
+/**
+ * Internal dependencies
+ */
+import PasswordStrengthMeter from '../../password-strength-meter';
+
+const CreatePassword = () => {
+ const [ passwordStrength, setPasswordStrength ] = useState( 0 );
+ const { customerPassword } = useSelect( ( select ) => {
+ const store = select( CHECKOUT_STORE_KEY );
+ return {
+ customerPassword: store.getCustomerPassword(),
+ };
+ } );
+ const { __internalSetCustomerPassword } = useDispatch( CHECKOUT_STORE_KEY );
+
+ return (
+ {
+ if (
+ validity.valueMissing ||
+ validity.badInput ||
+ validity.typeMismatch
+ ) {
+ return __( 'Please enter a valid password', 'woocommerce' );
+ }
+ } }
+ customValidation={ ( inputObject ) => {
+ if ( passwordStrength < 2 ) {
+ inputObject.setCustomValidity(
+ __( 'Please create a stronger password', 'woocommerce' )
+ );
+ return false;
+ }
+ return true;
+ } }
+ onChange={ ( value: string ) =>
+ __internalSetCustomerPassword( value )
+ }
+ feedback={
+
+ setPasswordStrength( strength )
+ }
+ />
+ }
+ />
+ );
+};
+
+export default CreatePassword;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/login-prompt.js b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/login-prompt.js
index 9ba2a8a8b17..6288a184025 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/login-prompt.js
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/login-prompt.js
@@ -21,12 +21,12 @@ const LoginPrompt = () => {
}
return (
- <>
- { __( 'Already have an account? ', 'woocommerce' ) }
-
- { __( 'Log in.', 'woocommerce' ) }
-
- >
+
+ { __( 'Log in', 'woocommerce' ) }
+
);
};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-fields-block/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-fields-block/style.scss
index 8bae04f028a..810d78d2d66 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-fields-block/style.scss
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-fields-block/style.scss
@@ -64,10 +64,15 @@
.wc-block-components-address-form__address_2-toggle {
background: none;
border: none;
+ color: inherit;
cursor: pointer;
- color: currentColor;
+ font-family: inherit;
font-size: inherit;
margin-top: $gap;
text-align: left;
width: 100%;
+
+ &:hover {
+ text-decoration: underline;
+ }
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-order-note-block/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-order-note-block/style.scss
index 9794c0889e2..ccc66f74af8 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-order-note-block/style.scss
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-order-note-block/style.scss
@@ -12,14 +12,13 @@
.wc-block-checkout__add-note .wc-block-components-textarea {
- border: 1px solid $universal-border-light;
margin-top: $gap;
&:focus {
background-color: #fff;
- color: currentColor;
+ color: $input-text-active;
outline: 0;
- box-shadow: 0 0 0 2px currentColor;
+ box-shadow: 0 0 0 1px $input-border-gray;
}
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-order-summary-block/test/block.js b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-order-summary-block/test/block.js
index 4dd13ee91d4..14e5a6be2c3 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-order-summary-block/test/block.js
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-order-summary-block/test/block.js
@@ -46,6 +46,19 @@ const defaultUseStoreCartValue = {
cartHasCalculatedShipping: mockPreviewCart.has_calculated_shipping,
};
+jest.mock( '@woocommerce/settings', () => ( {
+ ...jest.requireActual( '@woocommerce/settings' ),
+ SITE_CURRENCY: {
+ code: 'USD',
+ symbol: '$',
+ thousandSeparator: ',',
+ decimalSeparator: '.',
+ minorUnit: 2,
+ prefix: '$',
+ suffix: '',
+ },
+} ) );
+
jest.mock( '@woocommerce/base-context/hooks', () => ( {
...jest.requireActual( '@woocommerce/base-context/hooks' ),
@@ -182,6 +195,15 @@ jest.mock( '@woocommerce/settings', () => {
return {
...originalModule,
+ SITE_CURRENCY: {
+ code: 'USD',
+ symbol: '$',
+ thousandSeparator: ',',
+ decimalSeparator: '.',
+ minorUnit: 2,
+ prefix: '$',
+ suffix: '',
+ },
getSetting: jest.fn().mockImplementation( ( setting, ...rest ) => {
if ( setting === 'couponsEnabled' ) {
return true;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-pickup-options-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-pickup-options-block/block.tsx
index 916662798cf..9d6984b6984 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-pickup-options-block/block.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-pickup-options-block/block.tsx
@@ -6,6 +6,7 @@ import {
useState,
useEffect,
useCallback,
+ useMemo,
createInterpolateElement,
} from '@wordpress/element';
import { useShippingData, useStoreCart } from '@woocommerce/base-context/hooks';
@@ -138,10 +139,12 @@ const renderPickupLocation = (
const Block = (): JSX.Element | null => {
const { shippingRates, selectShippingRate } = useShippingData();
- // Get pickup locations from the first shipping package.
- const pickupLocations = ( shippingRates[ 0 ]?.shipping_rates || [] ).filter(
- isPackageRateCollectable
- );
+ // Memoize pickup locations to prevent re-rendering when the shipping rates change.
+ const pickupLocations = useMemo( () => {
+ return ( shippingRates[ 0 ]?.shipping_rates || [] ).filter(
+ isPackageRateCollectable
+ );
+ }, [ shippingRates ] );
const [ selectedOption, setSelectedOption ] = useState< string >(
() => pickupLocations.find( ( rate ) => rate.selected )?.rate_id || ''
@@ -168,13 +171,19 @@ const Block = (): JSX.Element | null => {
renderPickupLocation,
};
- // Update the selected option if there is no rate selected on mount.
useEffect( () => {
- if ( ! selectedOption && pickupLocations[ 0 ] ) {
+ if (
+ ! selectedOption &&
+ pickupLocations[ 0 ] &&
+ selectedOption !== pickupLocations[ 0 ].rate_id
+ ) {
setSelectedOption( pickupLocations[ 0 ].rate_id );
onSelectRate( pickupLocations[ 0 ].rate_id );
}
- }, [ onSelectRate, pickupLocations, selectedOption ] );
+ // Removing onSelectRate as it lead to an infinite loop when only one pickup location is available.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [ pickupLocations, selectedOption ] );
+
const packageCount = getShippingRatesPackageCount( shippingRates );
return (
<>
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/customer-address.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/customer-address.tsx
index 557010288e4..1c21625c188 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/customer-address.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/customer-address.tsx
@@ -82,9 +82,10 @@ const CustomerAddress = ( {
setEditing( true );
} }
fieldConfig={ addressFieldsConfig }
+ isExpanded={ editing }
/>
),
- [ shippingAddress, addressFieldsConfig ]
+ [ shippingAddress, addressFieldsConfig, editing ]
);
const renderAddressFormComponent = useCallback(
@@ -96,9 +97,10 @@ const CustomerAddress = ( {
values={ shippingAddress }
fields={ ADDRESS_FORM_KEYS }
fieldConfig={ addressFieldsConfig }
+ isEditing={ editing }
/>
),
- [ addressFieldsConfig, onChangeAddress, shippingAddress ]
+ [ addressFieldsConfig, onChangeAddress, shippingAddress, editing ]
);
return (
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/style.scss
index 1ad07b1ccd6..f5adbada0f2 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/style.scss
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-method-block/style.scss
@@ -44,8 +44,13 @@
color: #333;
}
&.wc-block-checkout__shipping-method-option--selected {
- outline: 2px solid currentColor !important; // Overwriting previous !important statement
+ outline: 1px solid $universal-border-strong;
background-color: $universal-background;
+
+ &:focus {
+ outline: 1px solid $universal-border-strong;
+ background-color: $universal-background;
+ }
}
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx
index 13633478b0c..80362a4b009 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx
@@ -26,6 +26,7 @@ import type {
} from '@woocommerce/types';
import NoticeBanner from '@woocommerce/base-components/notice-banner';
import type { ReactElement } from 'react';
+import { useMemo } from '@wordpress/element';
/**
* Renders a shipping rate control option.
@@ -73,19 +74,22 @@ const Block = ( { noShippingPlaceholder = null } ): ReactElement | null => {
const { shippingAddress } = useCustomerData();
- const filteredShippingRates = isCollectable
- ? shippingRates.map( ( shippingRatesPackage ) => {
- return {
- ...shippingRatesPackage,
- shipping_rates: shippingRatesPackage.shipping_rates.filter(
- ( shippingRatesPackageRate ) =>
- ! hasCollectableRate(
- shippingRatesPackageRate.method_id
- )
- ),
- };
- } )
- : shippingRates;
+ const filteredShippingRates = useMemo( () => {
+ return isCollectable
+ ? shippingRates.map( ( shippingRatesPackage ) => {
+ return {
+ ...shippingRatesPackage,
+ shipping_rates:
+ shippingRatesPackage.shipping_rates.filter(
+ ( shippingRatesPackageRate ) =>
+ ! hasCollectableRate(
+ shippingRatesPackageRate.method_id
+ )
+ ),
+ };
+ } )
+ : shippingRates;
+ }, [ shippingRates, isCollectable ] );
if ( ! needsShipping ) {
return null;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/password-strength-meter/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/password-strength-meter/index.tsx
new file mode 100644
index 00000000000..e37d7b4cd55
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/password-strength-meter/index.tsx
@@ -0,0 +1,119 @@
+/**
+ * External dependencies
+ */
+import { sprintf, __ } from '@wordpress/i18n';
+import { useInstanceId } from '@wordpress/compose';
+import { passwordStrength } from 'check-password-strength';
+import { usePrevious } from '@woocommerce/base-hooks';
+import { useEffect } from '@wordpress/element';
+import clsx from 'clsx';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+
+declare global {
+ interface Window {
+ zxcvbn: ( password: string ) => {
+ score: number;
+ };
+ }
+}
+
+const scoreDescriptions = [
+ __( 'Too weak', 'woocommerce' ),
+ __( 'Weak', 'woocommerce' ),
+ __( 'Medium', 'woocommerce' ),
+ __( 'Strong', 'woocommerce' ),
+ __( 'Very strong', 'woocommerce' ),
+];
+
+/**
+ * Renders a password strength meter.
+ *
+ * Uses zxcvbn to calculate the password strength if available, otherwise falls back to check-password-strength which
+ * does not include dictionaries of common passwords.
+ */
+const PasswordStrengthMeter = ( {
+ password = '',
+ onChange,
+}: {
+ password: string;
+ onChange?: ( strength: number ) => void;
+} ): React.ReactElement | null => {
+ const instanceId = useInstanceId(
+ PasswordStrengthMeter,
+ 'woocommerce-password-strength-meter'
+ ) as string;
+
+ let strength = -1;
+
+ if ( password.length > 0 ) {
+ if ( typeof window.zxcvbn === 'undefined' ) {
+ const result = passwordStrength( password );
+ strength = result.id;
+ } else {
+ const result = window.zxcvbn( password );
+ strength = result.score;
+ }
+ }
+
+ const previousStrength = usePrevious( strength );
+
+ useEffect( () => {
+ if ( strength !== previousStrength && onChange ) {
+ onChange( strength );
+ }
+ }, [ strength, previousStrength, onChange ] );
+
+ return (
+
+
+ { __( 'Password strength', 'woocommerce' ) }
+
+
-1 ? strength : 0 }
+ >
+ { scoreDescriptions[ strength ] ?? '' }
+
+
+ { !! scoreDescriptions[ strength ] && (
+ <>
+
+ { sprintf(
+ /* translators: %s: Password strength */
+ __(
+ 'Password strength: %1$s (%2$d characters long)',
+ 'woocommerce'
+ ),
+ scoreDescriptions[ strength ],
+ password.length
+ ) }
+ { ' ' }
+
+ { scoreDescriptions[ strength ] }
+
+ >
+ ) }
+
+
+ );
+};
+
+export default PasswordStrengthMeter;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/password-strength-meter/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/checkout/password-strength-meter/style.scss
new file mode 100644
index 00000000000..ffa5f7652d7
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/password-strength-meter/style.scss
@@ -0,0 +1,73 @@
+.wc-block-components-password-strength {
+ &.hidden {
+ opacity: 0;
+ }
+ .wc-block-components-password-strength__meter {
+ margin: $gap-small 0 0;
+ width: 100%;
+ display: block;
+ height: 6px;
+ border-radius: 4px;
+ border: 0;
+ background-color: $universal-border-light;
+ color: $alert-red;
+
+ &::-webkit-meter-bar,
+ &::-webkit-meter-inner-element {
+ background: none;
+ border: 0;
+ height: 6px;
+ vertical-align: middle;
+ }
+ &::-webkit-meter-optimum-value,
+ &::-webkit-meter-even-less-good-value,
+ &::-webkit-meter-suboptimum-value {
+ background: none;
+ background-color: currentColor;
+ transition: 0.2s ease;
+ border-radius: 3px;
+ border: 0;
+ height: 6px;
+ vertical-align: middle;
+ }
+
+ &:-moz-meter-optimum::-moz-meter-bar,
+ &:-moz-meter-sub-optimum::-moz-meter-bar,
+ &:-moz-meter-sub-sub-optimum::-moz-meter-bar {
+ background: none;
+ background-color: currentColor;
+ transition: 0.2s ease;
+ border-radius: 3px;
+ border: 0;
+ height: 6px;
+ vertical-align: middle;
+ }
+ }
+ .wc-block-components-password-strength__result {
+ @include font-size(smaller);
+ display: block;
+ text-align: right;
+ margin: $gap-smallest 0 0;
+ color: $alert-red;
+
+ &::after {
+ content: "\00a0 ";
+ }
+ }
+ .wc-block-components-password-strength__meter[value="1"],
+ .wc-block-components-password-strength__meter[value="1"] + .wc-block-components-password-strength__result {
+ color: $alert-red;
+ }
+ .wc-block-components-password-strength__meter[value="2"],
+ .wc-block-components-password-strength__meter[value="2"] + .wc-block-components-password-strength__result {
+ color: #ff6f00;
+ }
+ .wc-block-components-password-strength__meter[value="3"],
+ .wc-block-components-password-strength__meter[value="3"] + .wc-block-components-password-strength__result {
+ color: $alert-yellow;
+ }
+ .wc-block-components-password-strength__meter[value="4"],
+ .wc-block-components-password-strength__meter[value="4"] + .wc-block-components-password-strength__result {
+ color: $alert-green;
+ }
+}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/styles/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/checkout/styles/style.scss
index b36850eae8e..b1ca21dd3ee 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/styles/style.scss
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/styles/style.scss
@@ -13,6 +13,18 @@
margin-bottom: 0;
}
}
+ .wc-block-checkout__login-prompt {
+ float: right;
+ margin-top: -$gap-large;
+ @include font-size(regular);
+ }
+ .wc-block-checkout__create-account {
+ margin-top: $gap-large !important;
+ }
+ .wc-block-checkout__guest-checkout-notice {
+ margin: $gap-smallest 0 0;
+ @include font-size(smaller);
+ }
}
.is-large {
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/test/block.js b/plugins/woocommerce-blocks/assets/js/blocks/checkout/test/block.js
index 09388b2f084..bb4e72e4564 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/test/block.js
+++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/test/block.js
@@ -98,7 +98,11 @@ const CheckoutBlock = () => {
-
+
@@ -148,7 +152,7 @@ describe( 'Testing Checkout', () => {
expect( fetchMock ).toHaveBeenCalledTimes( 1 );
} );
- it( 'Renders the address card if the address is filled', async () => {
+ it( 'Renders the shipping address card if the address is filled and the cart contains a shippable product', async () => {
act( () => {
const cartWithAddress = {
...previewCart,
@@ -190,7 +194,7 @@ describe( 'Testing Checkout', () => {
await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
expect(
- screen.getByRole( 'button', { name: 'Edit address' } )
+ screen.getByRole( 'button', { name: 'Edit shipping address' } )
).toBeInTheDocument();
expect(
@@ -258,6 +262,30 @@ describe( 'Testing Checkout', () => {
expect( fetchMock ).toHaveBeenCalledTimes( 1 );
} );
+ it( 'Renders the billing address card if the address is filled and the cart contains a virtual product', async () => {
+ act( () => {
+ const cartWithVirtualProduct = {
+ ...previewCart,
+ needs_shipping: false,
+ };
+ fetchMock.mockResponse( ( req ) => {
+ if ( req.url.match( /wc\/store\/v1\/cart/ ) ) {
+ return Promise.resolve(
+ JSON.stringify( cartWithVirtualProduct )
+ );
+ }
+ return Promise.resolve( '' );
+ } );
+ } );
+ render( );
+
+ await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
+
+ expect(
+ screen.getByRole( 'button', { name: 'Edit billing address' } )
+ ).toBeInTheDocument();
+ } );
+
it( 'Ensures checkbox labels have unique IDs', async () => {
await act( async () => {
// Set required settings
@@ -327,4 +355,42 @@ describe( 'Testing Checkout', () => {
expect( formStepsWithNumber.length ).not.toBe( 0 );
} );
+
+ it( 'Shows guest checkout text', async () => {
+ await act( async () => {
+ allSettings.checkoutAllowsGuest = true;
+ allSettings.checkoutAllowsSignup = true;
+ dispatch( CHECKOUT_STORE_KEY ).__internalSetCustomerId( 0 );
+ } );
+
+ // Render the CheckoutBlock
+ const { rerender, queryByText } = render( );
+
+ // Wait for the component to fully load, assuming fetch calls or state updates
+ await waitFor( () => expect( fetchMock ).toHaveBeenCalled() );
+
+ // Query the text.
+ expect(
+ queryByText( /You are currently checking out as a guest./i )
+ ).toBeInTheDocument();
+
+ await act( async () => {
+ allSettings.checkoutAllowsGuest = true;
+ allSettings.checkoutAllowsSignup = true;
+ dispatch( CHECKOUT_STORE_KEY ).__internalSetCustomerId( 1 );
+ } );
+
+ rerender( );
+
+ expect(
+ queryByText( /You are currently checking out as a guest./i )
+ ).not.toBeInTheDocument();
+
+ await act( async () => {
+ // Restore initial settings
+ allSettings.checkoutAllowsGuest = undefined;
+ allSettings.checkoutAllowsSignup = undefined;
+ dispatch( CHECKOUT_STORE_KEY ).__internalSetCustomerId( 1 );
+ } );
+ } );
} );
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/deprecated.tsx b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/deprecated.tsx
new file mode 100644
index 00000000000..25b06189133
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/deprecated.tsx
@@ -0,0 +1,39 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
+/**
+ * Internal dependencies
+ */
+import { generateStyles } from './styles';
+import metadata from './block.json';
+
+const v1 = {
+ attributes: metadata.attributes,
+ supports: metadata.supports,
+ save: ( {
+ attributes,
+ }: {
+ attributes: { color: string; storeOnly: boolean };
+ } ) => {
+ const { color, storeOnly } = attributes;
+ const blockProps = { ...useBlockProps.save() };
+ if ( storeOnly ) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ );
+ },
+};
+
+export default [ v1 ];
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/edit.tsx
index 0e5c6139465..44153165732 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/edit.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/edit.tsx
@@ -8,24 +8,28 @@ import {
InnerBlocks,
} from '@wordpress/block-editor';
import { PanelBody, ColorPicker } from '@wordpress/components';
+import { type BlockEditProps } from '@wordpress/blocks';
+
+export type Attributes = {
+ color: string;
+ storeOnly: boolean;
+};
+
+export type EditProps = BlockEditProps< Attributes >;
/**
* Internal dependencies
*/
-import { generateStyles } from './styles';
-export default function Edit( { attributes, setAttributes } ) {
+export default function Edit( { attributes, setAttributes }: EditProps ) {
const { color, storeOnly } = attributes;
const blockProps = { ...useBlockProps() };
if ( storeOnly ) {
return (
- <>
-
-
-
-
- >
+
+
+
);
}
@@ -35,6 +39,7 @@ export default function Edit( { attributes, setAttributes } ) {
setAttributes( { color: newColor } )
}
@@ -45,8 +50,8 @@ export default function Edit( { attributes, setAttributes } ) {
+
-
>
);
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/entire-site.scss b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/entire-site.scss
new file mode 100644
index 00000000000..7ad342e5079
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/entire-site.scss
@@ -0,0 +1,175 @@
+body:has(.woocommerce-coming-soon-entire-site),
+body:has(.woocommerce-coming-soon-banner) {
+ margin: 0;
+ background-color: var(--woocommerce-coming-soon-color);
+ font-family: Inter, sans-serif;
+ min-width: 320px;
+ --wp--preset--color--contrast: #111;
+ --wp--style--global--wide-size: 1280px;
+
+ /* Reset */
+ h1,
+ p,
+ a {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ vertical-align: baseline;
+ }
+
+ ol,
+ ul {
+ list-style: none;
+ }
+
+ a {
+ text-decoration: none;
+ }
+
+ .is-layout-constrained > .alignwide {
+ margin: 0 auto;
+ }
+
+ .woocommerce-coming-soon-banner.alignwide {
+ max-width: 820px;
+ }
+
+
+ .wp-container-core-group-is-layout-4 {
+ &.wp-container-core-group-is-layout-4 {
+ justify-content: space-between;
+ }
+ }
+
+ .is-layout-flex {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ margin: 0;
+ }
+
+ .wp-block-site-title {
+ p {
+ line-height: normal;
+ }
+
+ a {
+ font-weight: 600;
+ font-size: 20px;
+ font-style: normal;
+ line-height: normal;
+ letter-spacing: -0.4px;
+ color: var(--wp--preset--color--contrast);
+ text-decoration: none;
+ }
+ }
+
+ .wp-block-social-links {
+ gap: 0.5em 18px;
+ }
+
+ .woocommerce-coming-soon-social-login {
+ gap: 48px;
+ }
+
+ .wp-block-loginout {
+ background-color: #000;
+ border-radius: 6px;
+ display: flex;
+ height: 40px;
+ width: 74px;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ box-sizing: border-box;
+
+ a {
+ color: #fff;
+ text-decoration: none;
+ line-height: 17px;
+ font-size: 14px;
+ font-weight: 500;
+ }
+ }
+
+ .wp-block-spacer {
+ margin: 0;
+ }
+
+ .woocommerce-coming-soon-banner-container {
+ padding-inline: min(5.5rem, 8vw);
+ margin: 0;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ @media (max-width: 660px) {
+ padding-inline: 0;
+ }
+
+ > .wp-block-group__inner-container {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ }
+ }
+
+ .woocommerce-coming-soon-powered-by-woo {
+ width: 100%;
+ --wp--preset--spacing--30: 0;
+ --wp--preset--spacing--10: 19px;
+
+ p {
+ font-style: normal;
+ font-weight: 400;
+ line-height: 19.2px;
+ letter-spacing: -0.12px;
+ color: #3c434a;
+ font-size: 12px;
+ font-family: Inter, sans-serif;
+ }
+
+ a {
+ font-family: Inter, sans-serif;
+ }
+ }
+
+ .coming-soon-is-vertically-aligned-center {
+ width: 100%;
+ align-items: stretch;
+ }
+
+ .coming-soon-cover {
+ .wp-block-cover__background {
+ background-color: var(--woocommerce-coming-soon-color) !important;
+ }
+ }
+
+ .woocommerce-coming-soon-header {
+ height: 40px;
+ }
+
+ h1.wp-block-heading.woocommerce-coming-soon-banner {
+ font-size: clamp(27px, 1.74rem + ((1vw - 3px) * 2), 48px);
+ font-weight: 400;
+ line-height: 58px;
+ font-family: Cardo, serif;
+ letter-spacing: normal;
+ text-align: center;
+ font-style: normal;
+ max-width: 820px;
+ color: var(--wp--preset--color--contrast);
+ margin: 0 auto;
+ text-wrap: balance;
+ }
+}
+
+.block-editor-block-preview__content-iframe {
+ body:has(.woocommerce-coming-soon-entire-site),
+ body:has(.woocommerce-coming-soon-banner) {
+ /* Set ratio to 1:1 so height is always equal to width for the preview. */
+ aspect-ratio: 1 / 1;
+ }
+}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/index.tsx
index a30310b2984..afcb33008ad 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/index.tsx
@@ -10,10 +10,14 @@ import { registerBlockType } from '@wordpress/blocks';
import Edit from './edit';
import Save from './save';
import metadata from './block.json';
+import deprecated from './deprecated';
+import './store-only.scss';
+import './entire-site.scss';
registerBlockType( metadata, {
title: __( 'Coming Soon', 'woocommerce' ),
edit: Edit,
save: Save,
apiVersion: 2,
+ deprecated,
} );
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/save.tsx b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/save.tsx
index 639f96b0430..779e1dfe88a 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/save.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/save.tsx
@@ -3,28 +3,10 @@
*/
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
-/**
- * Internal dependencies
- */
-import { generateStyles } from './styles';
-
-export default function Save( { attributes } ) {
- const { color, storeOnly } = attributes;
- const blockProps = { ...useBlockProps.save() };
-
- if ( storeOnly ) {
- return (
-
-
-
-
- );
- }
-
+export default function Save() {
return (
-
+
-
);
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/store-only.scss b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/store-only.scss
new file mode 100644
index 00000000000..4b851bc2b45
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/store-only.scss
@@ -0,0 +1,5 @@
+body:has(.woocommerce-coming-soon-store-only) {
+ .woocommerce-breadcrumb {
+ display: none;
+ }
+}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/styles.tsx b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/styles.tsx
index 18d18806918..3d4b505cfdf 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/styles.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/coming-soon/styles.tsx
@@ -1,3 +1,4 @@
+// Deprecated styles since 9.2. Please use *.scss file instead.
export const generateStyles = ( color = '#bea0f2' ) => {
return `
/* Reset */
@@ -122,7 +123,7 @@ export const generateStyles = ( color = '#bea0f2' ) => {
.woocommerce-coming-soon-header {
height: 40px;
}
- h1.wp-block-heading.woocommerce-coming-soon-banner {
+ .woocommerce-coming-soon-banner {
font-size: clamp(27px, 1.74rem + ((1vw - 3px) * 2), 48px);
font-weight: 400;
line-height: 58px;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/block.tsx
index 59e33ddcd74..77b4097c39c 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/block.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/block.tsx
@@ -20,15 +20,10 @@ import {
isCartResponseTotals,
isNumber,
} from '@woocommerce/types';
-import {
- unmountComponentAtNode,
- useCallback,
- useEffect,
- useRef,
- useState,
-} from '@wordpress/element';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import { sprintf, _n } from '@wordpress/i18n';
import clsx from 'clsx';
+import type { ReactRootWithContainer } from '@woocommerce/base-utils';
/**
* Internal dependencies
@@ -110,6 +105,8 @@ const MiniCartBlock = ( attributes: Props ): JSX.Element => {
setContentsNode( node );
}, [] );
+ const rootRef = useRef< ReactRootWithContainer[] | null >( null );
+
useEffect( () => {
const body = document.querySelector( 'body' );
if ( body ) {
@@ -134,7 +131,7 @@ const MiniCartBlock = ( attributes: Props ): JSX.Element => {
return;
}
if ( isOpen ) {
- renderParentBlock( {
+ const renderedBlock = renderParentBlock( {
Block: MiniCartContentsBlock,
blockName,
getProps: ( el: Element ) => {
@@ -151,16 +148,25 @@ const MiniCartBlock = ( attributes: Props ): JSX.Element => {
selector: '.wp-block-woocommerce-mini-cart-contents',
blockMap: getRegisteredBlockComponents( blockName ),
} );
+ rootRef.current = renderedBlock;
}
}
return () => {
if ( contentsNode instanceof Element && isOpen ) {
- const container = contentsNode.querySelector(
+ const unmountingContainer = contentsNode.querySelector(
'.wp-block-woocommerce-mini-cart-contents'
);
- if ( container ) {
- unmountComponentAtNode( container );
+
+ if ( unmountingContainer ) {
+ const foundRoot = rootRef?.current?.find(
+ ( { container } ) => unmountingContainer === container
+ );
+ if ( typeof foundRoot?.root?.unmount === 'function' ) {
+ setTimeout( () => {
+ foundRoot.root.unmount();
+ } );
+ }
}
}
};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/test/block.js b/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/test/block.js
index 313a7c3f3af..94a9644b75b 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/test/block.js
+++ b/plugins/woocommerce-blocks/assets/js/blocks/mini-cart/test/block.js
@@ -111,13 +111,6 @@ describe( 'Testing Mini-Cart', () => {
await waitFor( () =>
expect( screen.getByText( /your cart/i ) ).toBeInTheDocument()
);
-
- // The opening of the drawer uses deprecated ReactDOM.render.
- expect( console ).toHaveErroredWith(
- `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot%s`,
- // The stack trace
- expect.any( String )
- );
} );
it( 'closes the drawer when clicking on the close button', async () => {
@@ -132,9 +125,11 @@ describe( 'Testing Mini-Cart', () => {
// Close drawer.
let closeButton = null;
+
await waitFor( () => {
closeButton = screen.getByLabelText( /close/i );
} );
+
if ( closeButton ) {
await act( async () => {
await user.click( closeButton );
@@ -146,13 +141,6 @@ describe( 'Testing Mini-Cart', () => {
screen.queryByText( /your cart/i )
).not.toBeInTheDocument();
} );
-
- // The opening of the drawer uses deprecated ReactDOM.render.
- expect( console ).toHaveErroredWith(
- `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot%s`,
- // The stack trace
- expect.any( String )
- );
} );
it( 'renders empty cart if there are no items in the cart', async () => {
@@ -167,13 +155,6 @@ describe( 'Testing Mini-Cart', () => {
} );
expect( fetchMock ).toHaveBeenCalledTimes( 1 );
-
- // The opening of the drawer uses deprecated ReactDOM.render.
- expect( console ).toHaveErroredWith(
- `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot%s`,
- // The stack trace
- expect.any( String )
- );
} );
it( 'updates contents when removed from cart event is triggered', async () => {
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/best-sellers.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/best-sellers.tsx
index 08398bd4f2e..1cadf8977cd 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/best-sellers.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/best-sellers.tsx
@@ -32,7 +32,7 @@ const attributes = {
perPage: 5,
pages: 1,
},
- hideControls: [ CoreFilterNames.ORDER ],
+ hideControls: [ CoreFilterNames.ORDER, CoreFilterNames.FILTERABLE ],
};
const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/featured.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/featured.tsx
index 7c5176108d3..bc4f73d9dd0 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/featured.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/featured.tsx
@@ -31,7 +31,7 @@ const attributes = {
perPage: 5,
pages: 1,
},
- hideControls: [ CoreFilterNames.FEATURED ],
+ hideControls: [ CoreFilterNames.FEATURED, CoreFilterNames.FILTERABLE ],
};
const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/new-arrivals.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/new-arrivals.tsx
index 9f3a3310a30..6171e87fb94 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/new-arrivals.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/new-arrivals.tsx
@@ -40,7 +40,7 @@ const attributes = {
value: '-7 days',
},
},
- hideControls: [ CoreFilterNames.ORDER ],
+ hideControls: [ CoreFilterNames.ORDER, CoreFilterNames.FILTERABLE ],
};
const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/on-sale.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/on-sale.tsx
index 9f8c0b502a6..214ec729fb5 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/on-sale.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/on-sale.tsx
@@ -34,7 +34,7 @@ const attributes = {
perPage: 5,
pages: 1,
},
- hideControls: [ CoreFilterNames.ON_SALE ],
+ hideControls: [ CoreFilterNames.ON_SALE, CoreFilterNames.FILTERABLE ],
};
const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/top-rated.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/top-rated.tsx
index 79b1e21c11d..4bbeecca87a 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/top-rated.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections/top-rated.tsx
@@ -35,7 +35,7 @@ const attributes = {
perPage: 5,
pages: 1,
},
- hideControls: [ CoreFilterNames.ORDER ],
+ hideControls: [ CoreFilterNames.ORDER, CoreFilterNames.FILTERABLE ],
};
const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts
index 2dcc28b0b5d..98a1d9be2f6 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/constants.ts
@@ -62,6 +62,7 @@ export const DEFAULT_QUERY: ProductCollectionQuery = {
woocommerceHandPickedProducts: [],
timeFrame: undefined,
priceRange: undefined,
+ filterable: false,
};
export const DEFAULT_ATTRIBUTES: Pick<
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
index 1c0ff47e2d0..aea5e9447e1 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
@@ -36,7 +36,10 @@ import {
import { setQueryAttribute, getDefaultSettings } from '../../utils';
import UpgradeNotice from './upgrade-notice';
import ColumnsControl from './columns-control';
-import InheritQueryControl from './inherit-query-control';
+import {
+ InheritQueryControl,
+ FilterableControl,
+} from './use-page-context-control';
import OrderByControl from './order-by-control';
import OnSaleControl from './on-sale-control';
import StockStatusControl from './stock-status-control';
@@ -79,10 +82,27 @@ const ProductCollectionInspectorControls = (
const showQueryControls = inherit === false;
const showInheritQueryControl =
isArchiveTemplate && shouldShowFilter( CoreFilterNames.INHERIT );
+ const showFilterableControl =
+ ! isArchiveTemplate && shouldShowFilter( CoreFilterNames.FILTERABLE );
const showOrderControl =
showQueryControls && shouldShowFilter( CoreFilterNames.ORDER );
- const showFeaturedControl = shouldShowFilter( CoreFilterNames.FEATURED );
const showOnSaleControl = shouldShowFilter( CoreFilterNames.ON_SALE );
+ const showStockStatusControl = shouldShowFilter(
+ CoreFilterNames.STOCK_STATUS
+ );
+ const showHandPickedProductsControl = shouldShowFilter(
+ CoreFilterNames.HAND_PICKED
+ );
+ const showKeywordControl = shouldShowFilter( CoreFilterNames.KEYWORD );
+ const showAttributesControl = shouldShowFilter(
+ CoreFilterNames.ATTRIBUTES
+ );
+ const showTaxonomyControls = shouldShowFilter( CoreFilterNames.TAXONOMY );
+ const showFeaturedControl = shouldShowFilter( CoreFilterNames.FEATURED );
+ const showCreatedControl = shouldShowFilter( CoreFilterNames.CREATED );
+ const showPriceRangeControl = shouldShowFilter(
+ CoreFilterNames.PRICE_RANGE
+ );
const setQueryAttributeBind = useMemo(
() => setQueryAttribute.bind( null, props ),
@@ -114,6 +134,9 @@ const ProductCollectionInspectorControls = (
{ showInheritQueryControl && (
) }
+ { showFilterableControl && (
+
+ ) }
{ showOrderControl && (
@@ -134,16 +157,30 @@ const ProductCollectionInspectorControls = (
{ showOnSaleControl && (
) }
-
-
-
-
-
+ { showStockStatusControl && (
+
+ ) }
+ { showHandPickedProductsControl && (
+
+ ) }
+ { showKeywordControl && (
+
+ ) }
+ { showAttributesControl && (
+
+ ) }
+ { showTaxonomyControls && (
+
+ ) }
{ showFeaturedControl && (
) }
-
-
+ { showCreatedControl && (
+
+ ) }
+ { showPriceRangeControl && (
+
+ ) }
) : null }
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/inherit-query-control.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/use-page-context-control.tsx
similarity index 72%
rename from plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/inherit-query-control.tsx
rename to plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/use-page-context-control.tsx
index 3fbf3cc24a1..af0ce97335a 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/inherit-query-control.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/inspector-controls/use-page-context-control.tsx
@@ -17,11 +17,14 @@ import {
*/
import {
CoreFilterNames,
- ProductCollectionQuery,
- QueryControlProps,
+ type ProductCollectionQuery,
+ type QueryControlProps,
} from '../../types';
import { DEFAULT_QUERY } from '../../constants';
-import { getDefaultValueOfInheritQueryFromTemplate } from '../../utils';
+import {
+ getDefaultValueOfInherit,
+ getDefaultValueOfFilterable,
+} from '../../utils';
const label = __( 'Sync with current query', 'woocommerce' );
@@ -50,6 +53,11 @@ const searchResultsHelpText = __(
'woocommerce'
);
+const filterableHelpText = __(
+ 'Adjust the displayed products depending on the current template and any applied query filters.',
+ 'woocommerce'
+);
+
const getHelpTextForTemplate = ( templateId: string ): string => {
if ( templateId.includes( '//taxonomy-product_cat' ) ) {
return productsByCategoryHelpText;
@@ -81,10 +89,7 @@ const InheritQueryControl = ( {
}
);
- const defaultValue = useMemo(
- () => getDefaultValueOfInheritQueryFromTemplate(),
- []
- );
+ const defaultValue = useMemo( () => getDefaultValueOfInherit(), [] );
const currentTemplateId = editSiteStore.getEditedPostId() as string;
const helpText = getHelpTextForTemplate( currentTemplateId );
@@ -128,4 +133,41 @@ const InheritQueryControl = ( {
);
};
-export default InheritQueryControl;
+const FilterableControl = ( {
+ setQueryAttribute,
+ trackInteraction,
+ query,
+}: QueryControlProps ) => {
+ const filterable = query?.filterable;
+
+ const defaultValue = useMemo( () => getDefaultValueOfFilterable(), [] );
+
+ return (
+
filterable !== defaultValue }
+ isShownByDefault
+ onDeselect={ () => {
+ setQueryAttribute( {
+ filterable: defaultValue,
+ } );
+ trackInteraction( CoreFilterNames.FILTERABLE );
+ } }
+ >
+ {
+ setQueryAttribute( {
+ filterable: value,
+ } );
+ trackInteraction( CoreFilterNames.FILTERABLE );
+ } }
+ />
+
+ );
+};
+
+export { FilterableControl, InheritQueryControl };
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/product-collection-content.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/product-collection-content.tsx
index b7a87bcb3e8..a973de10df1 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/product-collection-content.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/edit/product-collection-content.tsx
@@ -23,7 +23,8 @@ import type {
} from '../types';
import { DEFAULT_ATTRIBUTES, INNER_BLOCKS_TEMPLATE } from '../constants';
import {
- getDefaultValueOfInheritQueryFromTemplate,
+ getDefaultValueOfInherit,
+ getDefaultValueOfFilterable,
useSetPreviewState,
} from '../utils';
import InspectorControls from './inspector-controls';
@@ -95,7 +96,8 @@ const ProductCollectionContent = ( {
...DEFAULT_ATTRIBUTES,
query: {
...( DEFAULT_ATTRIBUTES.query as ProductCollectionQuery ),
- inherit: getDefaultValueOfInheritQueryFromTemplate(),
+ inherit: getDefaultValueOfInherit(),
+ filterable: getDefaultValueOfFilterable(),
},
...( attributes as Partial< ProductCollectionAttributes > ),
queryId,
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
index 1c884c513d1..c772de36dac 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/types.ts
@@ -28,6 +28,7 @@ export interface ProductCollectionAttributes {
*/
queryContextIncludes: string[];
forcePageReload: boolean;
+ filterable: boolean;
// eslint-disable-next-line @typescript-eslint/naming-convention
__privatePreviewState?: PreviewState;
}
@@ -93,6 +94,7 @@ export interface ProductCollectionQuery {
isProductCollectionBlock: boolean;
woocommerceHandPickedProducts: string[];
priceRange: undefined | PriceRange;
+ filterable: boolean;
}
export type ProductCollectionEditComponentProps =
@@ -118,13 +120,15 @@ export type ProductCollectionSetAttributes = (
attrs: Partial< ProductCollectionAttributes >
) => void;
+export type TrackInteraction = ( filter: CoreFilterNames | string ) => void;
+
export type DisplayLayoutControlProps = {
displayLayout: ProductCollectionDisplayLayout;
setAttributes: ProductCollectionSetAttributes;
};
export type QueryControlProps = {
query: ProductCollectionQuery;
- trackInteraction: ( filter: CoreFilterNames | string ) => void;
+ trackInteraction: TrackInteraction;
setQueryAttribute: ( attrs: Partial< ProductCollectionQuery > ) => void;
};
@@ -150,6 +154,7 @@ export enum CoreFilterNames {
STOCK_STATUS = 'stock-status',
TAXONOMY = 'taxonomy',
PRICE_RANGE = 'price-range',
+ FILTERABLE = 'filterable',
}
export type CollectionName = CoreCollectionNames | string;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/utils.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/utils.tsx
index 7e0a795d454..2bae199f8f6 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-collection/utils.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-collection/utils.tsx
@@ -60,10 +60,17 @@ export function setQueryAttribute(
const isInProductArchive = () => {
const ARCHIVE_PRODUCT_TEMPLATES = [
'woocommerce/woocommerce//archive-product',
- 'woocommerce/woocommerce//taxonomy-product_cat',
- 'woocommerce/woocommerce//taxonomy-product_tag',
'woocommerce/woocommerce//taxonomy-product_attribute',
'woocommerce/woocommerce//product-search-results',
+ // Custom taxonomy templates have structure:
+ // <
>//taxonomy-product_cat-<>
+ // hence we're checking if template ID includes the middle part.
+ //
+ // That includes:
+ // - woocommerce/woocommerce//taxonomy-product_cat
+ // - woocommerce/woocommerce//taxonomy-product_tag
+ '//taxonomy-product_cat',
+ '//taxonomy-product_tag',
];
const currentTemplateId = select(
@@ -75,12 +82,18 @@ const isInProductArchive = () => {
* We want inherit value to be true when block is added to ARCHIVE_PRODUCT_TEMPLATES
* and false when added to somewhere else.
*/
- return currentTemplateId
- ? ARCHIVE_PRODUCT_TEMPLATES.includes( currentTemplateId )
- : false;
+ if ( currentTemplateId ) {
+ return ARCHIVE_PRODUCT_TEMPLATES.some( ( template ) =>
+ currentTemplateId.includes( template )
+ );
+ }
+
+ return false;
};
-const isFirstBlockThatSyncsWithQuery = () => {
+const isFirstBlockThatUsesPageContext = (
+ property: 'inherit' | 'filterable'
+) => {
// We use experimental selector because it's been graduated as stable (`getBlocksByName`)
// in Gutenberg 17.6 (https://github.com/WordPress/gutenberg/pull/58156) and will be
// available in WordPress 6.5.
@@ -97,15 +110,23 @@ const isFirstBlockThatSyncsWithQuery = () => {
( clientId ) => {
const block = getBlock( clientId );
- return block.attributes?.query?.inherit;
+ return block.attributes?.query?.[ property ];
}
);
return ! blockAlreadySyncedWithQuery;
};
-export function getDefaultValueOfInheritQueryFromTemplate() {
- return isInProductArchive() ? isFirstBlockThatSyncsWithQuery() : false;
+export function getDefaultValueOfInherit() {
+ return isInProductArchive()
+ ? isFirstBlockThatUsesPageContext( 'inherit' )
+ : false;
+}
+
+export function getDefaultValueOfFilterable() {
+ return ! isInProductArchive()
+ ? isFirstBlockThatUsesPageContext( 'filterable' )
+ : false;
}
/**
@@ -199,17 +220,18 @@ export const useSetPreviewState = ( {
const isGenericArchiveTemplate =
location.type === LocationType.Archive &&
location.sourceData?.termId === null;
- if ( isGenericArchiveTemplate ) {
- setAttributes( {
- __privatePreviewState: {
- isPreview: !! attributes?.query?.inherit,
- previewMessage: __(
- 'Actual products will vary depending on the page being viewed.',
- 'woocommerce'
- ),
- },
- } );
- }
+
+ setAttributes( {
+ __privatePreviewState: {
+ isPreview: isGenericArchiveTemplate
+ ? !! attributes?.query?.inherit
+ : false,
+ previewMessage: __(
+ 'Actual products will vary depending on the page being viewed.',
+ 'woocommerce'
+ ),
+ },
+ } );
}
}, [
attributes?.query?.inherit,
@@ -226,7 +248,8 @@ export const getDefaultQuery = (
...currentQuery,
orderBy: DEFAULT_QUERY.orderBy as TProductCollectionOrderBy,
order: DEFAULT_QUERY.order as TProductCollectionOrder,
- inherit: getDefaultValueOfInheritQueryFromTemplate(),
+ inherit: getDefaultValueOfInherit(),
+ filterable: getDefaultValueOfFilterable(),
} );
export const getDefaultDisplayLayout = () =>
@@ -246,7 +269,8 @@ export const getDefaultProductCollection = () =>
...DEFAULT_ATTRIBUTES,
query: {
...DEFAULT_ATTRIBUTES.query,
- inherit: getDefaultValueOfInheritQueryFromTemplate(),
+ inherit: getDefaultValueOfInherit(),
+ filterable: getDefaultValueOfFilterable(),
},
},
createBlocksFromInnerBlocksTemplate( INNER_BLOCKS_TEMPLATE )
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/block-variations.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/block-variations.tsx
index 042f578f422..3676b32a6c5 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/block-variations.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/block-variations.tsx
@@ -3,15 +3,13 @@
*/
import { __ } from '@wordpress/i18n';
import { BlockVariation } from '@wordpress/blocks';
-
-/**
- * Internal dependencies
- */
-import { activeFiltersIcon } from './inner-blocks/active-filters/icon';
-import { attributeFilterIcon } from './inner-blocks/attribute-filter/icon';
-import { priceFilterIcon } from './inner-blocks/price-filter/icon';
-import { ratingFilterIcon } from './inner-blocks/rating-filter/icon';
-import { stockStatusFilterIcon } from './inner-blocks/stock-filter/icon';
+import {
+ productFilterActive,
+ productFilterAttribute,
+ productFilterPrice,
+ productFilterRating,
+ productFilterStockStatus,
+} from '@woocommerce/icons';
const variations: BlockVariation[] = [
{
@@ -25,9 +23,7 @@ const variations: BlockVariation[] = [
heading: __( 'Active filters', 'woocommerce' ),
filterType: 'active-filters',
},
- icon: {
- src: activeFiltersIcon,
- },
+ icon: productFilterActive,
isDefault: true,
},
{
@@ -41,9 +37,7 @@ const variations: BlockVariation[] = [
filterType: 'price-filter',
heading: __( 'Price', 'woocommerce' ),
},
- icon: {
- src: priceFilterIcon,
- },
+ icon: productFilterPrice,
},
{
name: 'product-filter-stock-status',
@@ -56,9 +50,7 @@ const variations: BlockVariation[] = [
filterType: 'stock-filter',
heading: __( 'Status', 'woocommerce' ),
},
- icon: {
- src: stockStatusFilterIcon,
- },
+ icon: productFilterStockStatus,
},
{
name: 'product-filter-attribute',
@@ -72,9 +64,7 @@ const variations: BlockVariation[] = [
heading: __( 'Attribute', 'woocommerce' ),
attributeId: 0,
},
- icon: {
- src: attributeFilterIcon,
- },
+ icon: productFilterAttribute,
},
{
name: 'product-filter-rating',
@@ -87,9 +77,7 @@ const variations: BlockVariation[] = [
filterType: 'rating-filter',
heading: __( 'Rating', 'woocommerce' ),
},
- icon: {
- src: ratingFilterIcon,
- },
+ icon: productFilterRating,
},
];
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/block.json
index b436ca15677..3cf41cf54aa 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/block.json
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/block.json
@@ -11,6 +11,7 @@
"reusable": false,
"inserter": false
},
+ "ancestor": [ "woocommerce/product-filters" ],
"usesContext": [ "query", "queryId" ],
"attributes": {
"filterType": {
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/active-filters/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/active-filters/block.json
index ccb72ffd3d3..045349fe65e 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/active-filters/block.json
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/active-filters/block.json
@@ -2,7 +2,7 @@
"$schema": "https://schemas.wp.org/trunk/block.json",
"name": "woocommerce/product-filter-active",
"version": "1.0.0",
- "title": "Product Filter: Active Filters (Experimental)",
+ "title": "Filter Options",
"description": "Display the currently active filters.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/active-filters/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/active-filters/index.tsx
index d47f7d6fe62..c1285586709 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/active-filters/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/active-filters/index.tsx
@@ -3,20 +3,18 @@
*/
import { registerBlockType } from '@wordpress/blocks';
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
+import { productFilterOptions } from '@woocommerce/icons';
/**
* Internal dependencies
*/
import metadata from './block.json';
import Edit from './edit';
-import { activeFiltersIcon } from './icon';
import './style.scss';
if ( isExperimentalBlocksEnabled() ) {
registerBlockType( metadata, {
- icon: {
- src: activeFiltersIcon,
- },
+ icon: productFilterOptions,
edit: Edit,
} );
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/block.json
index fe9e17246ca..6a497c20a33 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/block.json
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/block.json
@@ -2,7 +2,7 @@
"$schema": "https://schemas.wp.org/trunk/block.json",
"name": "woocommerce/product-filter-attribute",
"version": "1.0.0",
- "title": "Product Filter: Attribute (Experimental)",
+ "title": "Filter Options",
"description": "Enable customers to filter the product grid by selecting one or more attributes, such as color.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
@@ -14,7 +14,45 @@
"inserter": false,
"color": {
"text": true,
- "background": false
+ "background": false,
+ "__experimentalDefaultControls": {
+ "text": false
+ }
+ },
+ "typography": {
+ "fontSize": true,
+ "lineHeight": true,
+ "__experimentalFontWeight": true,
+ "__experimentalFontFamily": true,
+ "__experimentalFontStyle": true,
+ "__experimentalTextTransform": true,
+ "__experimentalTextDecoration": true,
+ "__experimentalLetterSpacing": true,
+ "__experimentalDefaultControls": {
+ "fontSize": false
+ }
+ },
+ "spacing": {
+ "margin": true,
+ "padding": true,
+ "blockGap": true,
+ "__experimentalDefaultControls": {
+ "margin": false,
+ "padding": false,
+ "blockGap": false
+ }
+ },
+ "__experimentalBorder": {
+ "color": true,
+ "radius": true,
+ "style": true,
+ "width": true,
+ "__experimentalDefaultControls": {
+ "color": false,
+ "radius": false,
+ "style": false,
+ "width": false
+ }
}
},
"usesContext": [ "query", "queryId" ],
@@ -42,6 +80,18 @@
"isPreview": {
"type": "boolean",
"default": false
+ },
+ "sortOrder": {
+ "type": "string",
+ "default": "count-desc"
+ },
+ "hideEmpty": {
+ "type": "boolean",
+ "default": true
+ },
+ "clearButton": {
+ "type": "boolean",
+ "default":true
}
}
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/components/inspector-controls.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/components/inspector-controls.tsx
deleted file mode 100644
index 7dfd23c4663..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/components/inspector-controls.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * External dependencies
- */
-import { __, _x } from '@wordpress/i18n';
-import { InspectorControls } from '@wordpress/block-editor';
-import {
- PanelBody,
- ToggleControl,
- // @ts-expect-error - no types.
- // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
- __experimentalToggleGroupControl as ToggleGroupControl,
- // @ts-expect-error - no types.
- // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
- __experimentalToggleGroupControlOption as ToggleGroupControlOption,
-} from '@wordpress/components';
-
-/**
- * Internal dependencies
- */
-import { AttributeSelectControls } from './attribute-select-controls';
-import { EditProps } from '../types';
-
-export const Inspector = ( { attributes, setAttributes }: EditProps ) => {
- const { attributeId, showCounts, queryType, displayStyle, selectType } =
- attributes;
-
- return (
-
-
-
- setAttributes( {
- showCounts: ! showCounts,
- } )
- }
- />
-
- setAttributes( {
- selectType: value,
- } )
- }
- className="wc-block-attribute-filter__multiple-toggle"
- >
-
-
-
- { selectType === 'multiple' && (
-
- setAttributes( {
- queryType: value,
- } )
- }
- className="wc-block-attribute-filter__conditions-toggle"
- >
-
-
-
- ) }
-
- setAttributes( {
- displayStyle: value,
- } )
- }
- className="wc-block-attribute-filter__display-toggle"
- >
-
-
-
-
-
- {
- setAttributes( {
- attributeId: id,
- } );
- } }
- />
-
-
- );
-};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/components/inspector.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/components/inspector.tsx
new file mode 100644
index 00000000000..77ab2844d2d
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/components/inspector.tsx
@@ -0,0 +1,160 @@
+/**
+ * External dependencies
+ */
+import { getSetting } from '@woocommerce/settings';
+import { AttributeSetting } from '@woocommerce/types';
+import { InspectorControls } from '@wordpress/block-editor';
+import { createInterpolateElement } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import {
+ ComboboxControl,
+ PanelBody,
+ SelectControl,
+ ToggleControl,
+ // @ts-expect-error - no types.
+ // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
+ __experimentalToggleGroupControl as ToggleGroupControl,
+ // @ts-expect-error - no types.
+ // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
+ __experimentalToggleGroupControlOption as ToggleGroupControlOption,
+} from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { sortOrderOptions } from '../constants';
+import { BlockAttributes, EditProps } from '../types';
+
+const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
+
+export const Inspector = ( { attributes, setAttributes }: EditProps ) => {
+ const {
+ attributeId,
+ sortOrder,
+ queryType,
+ displayStyle,
+ showCounts,
+ hideEmpty,
+ clearButton,
+ } = attributes;
+
+ return (
+ <>
+
+
+ ( {
+ value: item.attribute_id,
+ label: item.attribute_label,
+ } ) ) }
+ value={ attributeId + '' }
+ onChange={ ( value ) =>
+ setAttributes( {
+ attributeId: parseInt( value || '', 10 ),
+ } )
+ }
+ help={ __(
+ 'Choose the attribute to show in this filter.',
+ 'woocommerce'
+ ) }
+ />
+
+
+
+ setAttributes( { sortOrder: value } )
+ }
+ help={ __(
+ 'Determine the order of filter options.',
+ 'woocommerce'
+ ) }
+ />
+
+ setAttributes( { queryType: value } )
+ }
+ style={ { width: '100%' } }
+ help={
+ queryType === 'and'
+ ? createInterpolateElement(
+ __(
+ 'Show results for all selected attributes. Displayed products must contain all of them to appear in the results.',
+ 'woocommerce'
+ ),
+ {
+ b: ,
+ }
+ )
+ : __(
+ 'Show results for any of the attributes selected (displayed products don’t have to have them all).',
+ 'woocommerce'
+ )
+ }
+ >
+
+
+
+
+
+
+
+ setAttributes( { displayStyle: value } ) }
+ style={ { width: '100%' } }
+ >
+
+
+
+
+ setAttributes( { showCounts: value } )
+ }
+ />
+
+ setAttributes( { hideEmpty: ! value } )
+ }
+ />
+
+ setAttributes( { clearButton: value } )
+ }
+ />
+
+
+ >
+ );
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/constants.ts
index f5114d59ea6..8c47d8c30e4 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/constants.ts
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/constants.ts
@@ -1,7 +1,12 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
export const attributeOptionsPreview = [
{
id: 23,
- name: 'Blue',
+ name: __( 'Blue', 'woocommerce' ),
slug: 'blue',
attr_slug: 'blue',
description: '',
@@ -10,7 +15,7 @@ export const attributeOptionsPreview = [
},
{
id: 29,
- name: 'Gray',
+ name: __( 'Gray', 'woocommerce' ),
slug: 'gray',
attr_slug: 'gray',
description: '',
@@ -19,7 +24,7 @@ export const attributeOptionsPreview = [
},
{
id: 24,
- name: 'Green',
+ name: __( 'Green', 'woocommerce' ),
slug: 'green',
attr_slug: 'green',
description: '',
@@ -28,7 +33,7 @@ export const attributeOptionsPreview = [
},
{
id: 25,
- name: 'Red',
+ name: __( 'Red', 'woocommerce' ),
slug: 'red',
attr_slug: 'red',
description: '',
@@ -37,7 +42,7 @@ export const attributeOptionsPreview = [
},
{
id: 30,
- name: 'Yellow',
+ name: __( 'Yellow', 'woocommerce' ),
slug: 'yellow',
attr_slug: 'yellow',
description: '',
@@ -45,3 +50,17 @@ export const attributeOptionsPreview = [
count: 1,
},
];
+
+export const sortOrders = {
+ 'name-asc': __( 'Name, A to Z', 'woocommerce' ),
+ 'name-desc': __( 'Name, Z to A', 'woocommerce' ),
+ 'count-desc': __( 'Most results first', 'woocommerce' ),
+ 'count-asc': __( 'Least results first', 'woocommerce' ),
+};
+
+export const sortOrderOptions = Object.entries( sortOrders ).map(
+ ( [ value, label ] ) => ( {
+ label,
+ value,
+ } )
+);
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/edit.tsx
index 2b4b26ff388..d535cee6a20 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/edit.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/edit.tsx
@@ -2,8 +2,8 @@
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
-import { useCallback, useEffect, useState } from '@wordpress/element';
-import { BlockControls, useBlockProps } from '@wordpress/block-editor';
+import { useEffect, useState } from '@wordpress/element';
+import { useBlockProps } from '@wordpress/block-editor';
import { getSetting } from '@woocommerce/settings';
import {
useCollection,
@@ -14,26 +14,16 @@ import {
AttributeTerm,
objectHasProp,
} from '@woocommerce/types';
-import {
- Disabled,
- Button,
- ToolbarGroup,
- withSpokenMessages,
- Notice,
-} from '@wordpress/components';
+import { Disabled, withSpokenMessages, Notice } from '@wordpress/components';
import { dispatch, useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { EditProps, isAttributeCounts } from './types';
-import {
- NoAttributesPlaceholder,
- AttributesPlaceholder,
-} from './components/placeholder';
-import { AttributeSelectControls } from './components/attribute-select-controls';
+import { NoAttributesPlaceholder } from './components/placeholder';
import { getAttributeFromId } from './utils';
-import { Inspector } from './components/inspector-controls';
+import { Inspector } from './components/inspector';
import { AttributeCheckboxList } from './components/attribute-checkbox-list';
import { AttributeDropdown } from './components/attribute-dropdown';
import { attributeOptionsPreview } from './constants';
@@ -41,84 +31,21 @@ import './style.scss';
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
-const Toolbar = ( {
- onClick,
- isEditing,
-}: {
- onClick: () => void;
- isEditing: boolean;
-} ) => (
-
-
-
-);
-
-const Wrapper = ( {
- children,
- onClickToolbarEdit,
- isEditing,
- blockProps,
-}: {
- children: React.ReactNode;
- onClickToolbarEdit: () => void;
- isEditing: boolean;
- blockProps: object;
-} ) => (
-
-
- { children }
-
-);
-
-const AttributeSelectPlaceholder = ( {
- attributeId,
- setAttributeId,
- onClickDone,
-}: {
- attributeId: number;
- setAttributeId: ( id: number ) => void;
- onClickDone: () => void;
-} ) => (
-
-
-
-
- { __( 'Done', 'woocommerce' ) }
-
-
-
-);
-
const Edit = ( props: EditProps ) => {
- const {
- attributes: blockAttributes,
- setAttributes,
- debouncedSpeak,
- clientId,
- } = props;
+ const { attributes: blockAttributes, clientId } = props;
- const { attributeId, queryType, isPreview, displayStyle, showCounts } =
- blockAttributes;
+ const {
+ attributeId,
+ queryType,
+ isPreview,
+ displayStyle,
+ showCounts,
+ sortOrder,
+ hideEmpty,
+ } = blockAttributes;
const attributeObject = getAttributeFromId( attributeId );
- const [ isEditing, setIsEditing ] = useState(
- ! attributeId && ! isPreview
- );
-
const [ attributeOptions, setAttributeOptions ] = useState<
AttributeTerm[]
>( [] );
@@ -130,7 +57,7 @@ const Edit = ( props: EditProps ) => {
resourceName: 'products/attributes/terms',
resourceValues: [ attributeObject?.id || 0 ],
shouldSelect: blockAttributes.attributeId > 0,
- query: { orderby: 'menu_order' },
+ query: { orderby: 'menu_order', hide_empty: hideEmpty },
} );
const { results: filteredCounts } = useCollectionData( {
@@ -183,8 +110,6 @@ const Edit = ( props: EditProps ) => {
[ clientId ]
);
- const blockProps = useBlockProps();
-
useEffect( () => {
const termIdHasProducts =
objectHasProp( filteredCounts, 'attribute_counts' ) &&
@@ -192,14 +117,31 @@ const Edit = ( props: EditProps ) => {
? filteredCounts.attribute_counts.map( ( term ) => term.term )
: [];
- if ( termIdHasProducts.length === 0 ) return setAttributeOptions( [] );
+ if ( termIdHasProducts.length === 0 && hideEmpty )
+ return setAttributeOptions( [] );
setAttributeOptions(
- attributeTerms.filter( ( term ) => {
- return termIdHasProducts.includes( term.id );
- } )
+ attributeTerms
+ .filter( ( term ) => {
+ if ( hideEmpty )
+ return termIdHasProducts.includes( term.id );
+ return true;
+ } )
+ .sort( ( a, b ) => {
+ switch ( sortOrder ) {
+ case 'name-asc':
+ return a.name > b.name ? 1 : -1;
+ case 'name-desc':
+ return a.name < b.name ? 1 : -1;
+ case 'count-asc':
+ return a.count > b.count ? 1 : -1;
+ case 'count-desc':
+ default:
+ return a.count < b.count ? 1 : -1;
+ }
+ } )
);
- }, [ attributeTerms, filteredCounts ] );
+ }, [ attributeTerms, filteredCounts, sortOrder, hideEmpty ] );
useEffect( () => {
if ( productFilterWrapperBlockId ) {
@@ -231,36 +173,16 @@ const Edit = ( props: EditProps ) => {
updateBlockAttributes,
] );
- const onClickDone = useCallback( () => {
- setIsEditing( false );
- debouncedSpeak(
- __(
- 'Now displaying a preview of the Filter Products by Attribute block.',
- 'woocommerce'
- )
- );
- }, [ setIsEditing ] );
-
- const setAttributeId = useCallback(
- ( id ) => {
- setAttributes( {
- attributeId: id,
- } );
- },
- [ setAttributes ]
+ const Wrapper = ( { children }: { children: React.ReactNode } ) => (
+
+
+ { children }
+
);
- const toggleEditing = useCallback( () => {
- setIsEditing( ! isEditing );
- }, [ isEditing ] );
-
if ( isPreview ) {
return (
-
+
{
// Block rendering starts.
if ( Object.keys( ATTRIBUTES ).length === 0 )
return (
-
+
);
- if ( isEditing )
- return (
-
-
-
- );
-
if ( ! attributeId || ! attributeObject )
return (
-
+
{ __(
@@ -318,11 +217,7 @@ const Edit = ( props: EditProps ) => {
if ( attributeOptions.length === 0 )
return (
-
+
{ __(
@@ -335,12 +230,7 @@ const Edit = ( props: EditProps ) => {
);
return (
-
-
+
{ displayStyle === 'dropdown' ? (
(
+ 'defaultProductFilterAttribute'
+ );
+
registerBlockType( metadata, {
edit: Edit,
- icon: attributeFilterIcon,
+ icon: productFilterOptions,
+ attributes: {
+ ...metadata.attributes,
+ attributeId: {
+ ...metadata.attributes.attributeId,
+ default: parseInt( defaultAttribute.attribute_id, 10 ),
+ },
+ },
} );
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/types.ts
index 440b5b0045b..30883d8a680 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/types.ts
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/types.ts
@@ -3,13 +3,21 @@
*/
import { BlockEditProps } from '@wordpress/blocks';
+/**
+ * Internal dependencies
+ */
+import { sortOrders } from './constants';
+
export type BlockAttributes = {
attributeId: number;
showCounts: boolean;
- queryType: string;
+ queryType: 'or' | 'and';
displayStyle: string;
selectType: string;
isPreview: boolean;
+ sortOrder: keyof typeof sortOrders;
+ hideEmpty: boolean;
+ clearButton: boolean;
};
export interface EditProps extends BlockEditProps< BlockAttributes > {
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/price-filter/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/price-filter/block.json
index 0ebb46a8e13..da2b28598c8 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/price-filter/block.json
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/price-filter/block.json
@@ -2,7 +2,7 @@
"$schema": "https://schemas.wp.org/trunk/block.json",
"name": "woocommerce/product-filter-price",
"version": "1.0.0",
- "title": "Product Filter: Price (Experimental)",
+ "title": "Filter Options",
"description": "Enable customers to filter the product collection by choosing a price range.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/price-filter/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/price-filter/index.tsx
index 5fd9d224956..3b8270241bc 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/price-filter/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/price-filter/index.tsx
@@ -3,6 +3,7 @@
*/
import { registerBlockType } from '@wordpress/blocks';
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
+import { productFilterOptions } from '@woocommerce/icons';
/**
* Internal dependencies
@@ -10,13 +11,10 @@ import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
import './style.scss';
import metadata from './block.json';
import Edit from './edit';
-import { priceFilterIcon } from './icon';
if ( isExperimentalBlocksEnabled() ) {
registerBlockType( metadata, {
- icon: {
- src: priceFilterIcon,
- },
+ icon: productFilterOptions,
edit: Edit,
} );
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/rating-filter/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/rating-filter/block.json
index 5811345e9a8..a1f35fb4845 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/rating-filter/block.json
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/rating-filter/block.json
@@ -1,7 +1,7 @@
{
"name": "woocommerce/product-filter-rating",
"version": "1.0.0",
- "title": "Product Filter: Rating (Experimental)",
+ "title": "Filter Options",
"description": "Enable customers to filter the product collection by rating.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/rating-filter/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/rating-filter/index.tsx
index 279a0d2437e..d6a2903f7f2 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/rating-filter/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/rating-filter/index.tsx
@@ -3,19 +3,17 @@
*/
import { registerBlockType } from '@wordpress/blocks';
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
+import { productFilterOptions } from '@woocommerce/icons';
/**
* Internal dependencies
*/
import edit from './edit';
import metadata from './block.json';
-import { ratingFilterIcon } from './icon';
if ( isExperimentalBlocksEnabled() ) {
registerBlockType( metadata, {
- icon: {
- src: ratingFilterIcon,
- },
+ icon: productFilterOptions,
attributes: {
...metadata.attributes,
},
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/stock-filter/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/stock-filter/block.json
index 24d964e2ea9..384b0186e14 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/stock-filter/block.json
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/stock-filter/block.json
@@ -1,7 +1,7 @@
{
"name": "woocommerce/product-filter-stock-status",
"version": "1.0.0",
- "title": "Product Filter: Stock Status (Experimental)",
+ "title": "Filter Options",
"description": "Enable customers to filter the product collection by stock status.",
"category": "woocommerce",
"keywords": [ "WooCommerce", "filter", "stock" ],
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/stock-filter/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/stock-filter/index.tsx
index 8ef9d721d0c..4b73575caa1 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/stock-filter/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/stock-filter/index.tsx
@@ -3,6 +3,7 @@
*/
import { registerBlockType } from '@wordpress/blocks';
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
+import { productFilterOptions } from '@woocommerce/icons';
/**
* Internal dependencies
@@ -10,13 +11,10 @@ import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
import './style.scss';
import edit from './edit';
import metadata from './block.json';
-import { stockStatusFilterIcon } from './icon';
if ( isExperimentalBlocksEnabled() ) {
registerBlockType( metadata, {
- icon: {
- src: stockStatusFilterIcon,
- },
+ icon: productFilterOptions,
edit,
} );
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters-overlay-navigation/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters-overlay-navigation/block.json
index 5ed551dfca9..007f8a3179f 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters-overlay-navigation/block.json
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters-overlay-navigation/block.json
@@ -1,6 +1,6 @@
{
"name": "woocommerce/product-filters-overlay-navigation",
- "title": "Navigation (Experimental)",
+ "title": "Overlay Navigation (Experimental)",
"description": "Display overlay navigation controls.",
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters-overlay-navigation/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters-overlay-navigation/index.tsx
index 04526b52289..01efbe5906f 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters-overlay-navigation/index.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters-overlay-navigation/index.tsx
@@ -2,7 +2,8 @@
* External dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
-import { Icon, navigation } from '@wordpress/icons';
+import { Icon } from '@wordpress/icons';
+import { closeSquareShadow } from '@woocommerce/icons';
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
/**
@@ -17,6 +18,6 @@ if ( isExperimentalBlocksEnabled() ) {
registerBlockType( metadata, {
edit: Edit,
save: Save,
- icon: ,
+ icon: ,
} );
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/block.json
index 2494ce94ea7..8465eedca18 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/block.json
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/block.json
@@ -20,6 +20,7 @@
"text": true
},
"multiple": false,
+ "inserter": false,
"interactivity": true,
"typography": {
"fontSize": true,
@@ -33,6 +34,8 @@
"verticalAlignment": "top"
},
"allowVerticalAlignment": true,
+ "allowJustification": true,
+ "allowOrientation": true,
"allowInheriting": false
},
"spacing": {
@@ -42,7 +45,24 @@
"textdomain": "woocommerce",
"usesContext": [ "postId" ],
"providesContext": {},
- "attributes": {},
+ "attributes": {
+ "overlay": {
+ "type": "string",
+ "default": "never"
+ },
+ "overlayIcon": {
+ "type": "string",
+ "default": "filter-icon-1"
+ },
+ "overlayButtonStyle": {
+ "type": "string",
+ "default": "label-icon"
+ },
+ "overlayIconSize": {
+ "type": "number",
+ "default": "12"
+ }
+ },
"viewScript": "wc-product-filters-frontend",
"example": {}
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx
index dd4293e48cb..c8c8c697caf 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx
@@ -1,21 +1,40 @@
/**
* External dependencies
*/
+import { filter, filterThreeLines } from '@woocommerce/icons';
+import { getSetting } from '@woocommerce/settings';
+import { AttributeSetting } from '@woocommerce/types';
import {
InnerBlocks,
+ InspectorControls,
useBlockProps,
useInnerBlocksProps,
} from '@wordpress/block-editor';
import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks';
-import { __, sprintf } from '@wordpress/i18n';
-import { useCollection } from '@woocommerce/base-context/hooks';
-import { AttributeTerm } from '@woocommerce/types';
-import { Spinner } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { Icon, menu, settings } from '@wordpress/icons';
+import {
+ ExternalLink,
+ PanelBody,
+ RadioControl,
+ RangeControl,
+ // @ts-expect-error - no types.
+ // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
+ __experimentalToggleGroupControl as ToggleGroupControl,
+ // @ts-expect-error - no types.
+ // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
+ __experimentalToggleGroupControlOption as ToggleGroupControlOption,
+} from '@wordpress/components';
/**
* Internal dependencies
*/
-import type { ProductFiltersBlockAttributes } from './types';
+import './editor.scss';
+import type { BlockAttributes } from './types';
+
+const defaultAttribute = getSetting< AttributeSetting >(
+ 'defaultProductFilterAttribute'
+);
const TEMPLATE: InnerBlockTemplate[] = [
[
@@ -51,8 +70,8 @@ const TEMPLATE: InnerBlockTemplate[] = [
'woocommerce/product-filter',
{
filterType: 'attribute-filter',
- heading: __( 'Attribute', 'woocommerce' ),
- attributeId: 0,
+ heading: defaultAttribute.attribute_label,
+ attributeId: parseInt( defaultAttribute.attribute_id, 10 ),
},
],
[
@@ -62,75 +81,199 @@ const TEMPLATE: InnerBlockTemplate[] = [
heading: __( 'Rating', 'woocommerce' ),
},
],
-];
-
-const addHighestProductCountAttributeToTemplate = (
- template: InnerBlockTemplate[],
- highestProductCountAttribute: AttributeTerm | null
-): InnerBlockTemplate[] => {
- if ( highestProductCountAttribute === null ) return template;
-
- return template.map( ( block ) => {
- const blockNameIndex = 0;
- const blockAttributesIndex = 1;
- const blockName = block[ blockNameIndex ];
- const blockAttributes = block[ blockAttributesIndex ];
- if (
- blockName === 'woocommerce/product-filter' &&
- blockAttributes?.filterType === 'attribute-filter'
- ) {
- return [
- blockName,
+ [
+ 'core/buttons',
+ { layout: { type: 'flex' } },
+ [
+ [
+ 'core/button',
{
- ...blockAttributes,
- heading: highestProductCountAttribute.name,
- attributeId: highestProductCountAttribute.id,
- metadata: {
- name: sprintf(
- /* translators: %s is referring to the filter attribute name. For example: Color, Size, etc. */
- __( '%s (Experimental)', 'woocommerce' ),
- highestProductCountAttribute.name
- ),
+ text: __( 'Apply', 'woocommerce' ),
+ className: 'wc-block-product-filters__apply-button',
+ style: {
+ border: {
+ width: '0px',
+ style: 'none',
+ },
+ typography: {
+ textDecoration: 'none',
+ },
+ outline: 'none',
+ fontSize: 'medium',
},
},
- ];
- }
+ ],
+ ],
+ ],
+];
- return block;
- } );
-};
-
-export const Edit = ( {}: BlockEditProps< ProductFiltersBlockAttributes > ) => {
+export const Edit = ( {
+ setAttributes,
+ attributes,
+}: BlockEditProps< BlockAttributes > ) => {
const blockProps = useBlockProps();
- const { results: attributes, isLoading } = useCollection< AttributeTerm >( {
- namespace: '/wc/store/v1',
- resourceName: 'products/attributes',
- } );
- const highestProductCountAttribute =
- attributes.reduce< AttributeTerm | null >(
- ( attributeWithMostProducts, attribute ) => {
- if ( attributeWithMostProducts === null ) {
- return attribute;
- }
- return attribute.count > attributeWithMostProducts.count
- ? attribute
- : attributeWithMostProducts;
- },
- null
- );
- const updatedTemplate = addHighestProductCountAttributeToTemplate(
- TEMPLATE,
- highestProductCountAttribute
+ const templatePartEditUri = getSetting< string >(
+ 'templatePartProductFiltersOverlayEditUri',
+ ''
);
- if ( isLoading ) {
- return ;
- }
-
return (
-
+
+
+ {
+ setAttributes( { overlay: value } );
+ } }
+ >
+
+
+
+
+ { attributes.overlay === 'mobile' && (
+ <>
+ {
+ setAttributes( {
+ overlayButtonStyle: value,
+ } );
+ } }
+ options={ [
+ {
+ value: 'label-icon',
+ label: __(
+ 'Label and icon',
+ 'woocommerce'
+ ),
+ },
+ {
+ value: 'label',
+ label: __(
+ 'Label only',
+ 'woocommerce'
+ ),
+ },
+ {
+ value: 'icon',
+ label: __( 'Icon only', 'woocommerce' ),
+ },
+ ] }
+ />
+ { attributes.overlayButtonStyle !== 'label' && (
+ <>
+ {
+ setAttributes( {
+ overlayIcon: value,
+ } );
+ } }
+ >
+
+ }
+ />
+
+ }
+ />
+
+ }
+ />
+
+ }
+ />
+
+
+ setAttributes( {
+ overlayIconSize: value,
+ } )
+ }
+ min={ 20 }
+ max={ 80 }
+ />
+ >
+ ) }
+ >
+ ) }
+ { attributes.overlay !== 'never' && (
+
+ { __( 'Edit overlay', 'woocommerce' ) }
+
+ ) }
+
+
+
);
};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/editor.scss
new file mode 100644
index 00000000000..c31c0066cd9
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/editor.scss
@@ -0,0 +1,47 @@
+$blue: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9));
+
+.wc-block-editor-product-filters__overlay-link {
+ border-style: solid;
+ border-color: $blue;
+ border-width: 1px;
+ border-radius: 2px;
+ padding: 10px 20px;
+ text-align: center;
+ display: block;
+ .components-external-link__contents {
+ text-decoration: none;
+ }
+}
+
+.wc-block-editor-product-filters__overlay-button-size {
+ .components-range-control__wrapper {
+ order: 1;
+ }
+
+ .components-range-control__number {
+ order: 0;
+ margin-left: 0 !important;
+ margin-right: 20px;
+
+ .components-input-control__input::-webkit-outer-spin-button,
+ .components-input-control__input::-webkit-inner-spin-button {
+ appearance: none;
+ margin: 0;
+ }
+
+ /* Remove stepper arrows in Firefox */
+ .components-input-control__input {
+ appearance: textfield;
+ width: 40px !important;
+ }
+
+ .components-input-control__container::after {
+ content: "px";
+ position: absolute;
+ top: 6px;
+ right: 7px;
+ font-size: 12px;
+ color: $blue;
+ }
+ }
+}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts
index 1a6550ead86..797d9028750 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts
@@ -1,7 +1,12 @@
-/**
- * Internal dependencies
- */
-
-export interface ProductFiltersBlockAttributes {
+export interface BlockAttributes {
productId?: string;
+ setAttributes: ( attributes: ProductFiltersBlockAttributes ) => void;
+ overlay: 'never' | 'mobile' | 'always';
+ overlayIcon:
+ | 'filter-icon-1'
+ | 'filter-icon-2'
+ | 'filter-icon-3'
+ | 'filter-icon-4';
+ overlayButtonStyle: 'label-icon' | 'label' | 'icon';
+ overlayIconSize?: number;
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-template/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-template/edit.tsx
index f78e0c58019..365eb3cbed0 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-template/edit.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-template/edit.tsx
@@ -25,7 +25,11 @@ import type { BlockEditProps, BlockInstance } from '@wordpress/blocks';
/**
* Internal dependencies
*/
-import { useGetLocation, useProductCollectionQueryContext } from './utils';
+import {
+ useGetLocation,
+ useProductCollectionQueryContext,
+ parseTemplateSlug,
+} from './utils';
import './editor.scss';
import { getDefaultStockStatuses } from '../product-collection/constants';
@@ -194,15 +198,6 @@ const ProductTemplateEdit = (
per_page: -1,
context: 'view',
} );
- const templateCategory =
- inherit &&
- templateSlug?.startsWith( 'category-' ) &&
- getEntityRecords( 'taxonomy', 'category', {
- context: 'view',
- per_page: 1,
- _fields: [ 'id' ],
- slug: templateSlug.replace( 'category-', '' ),
- } );
const query: Record< string, unknown > = {
postType,
offset: perPage ? perPage * ( page - 1 ) + offset : 0,
@@ -240,8 +235,29 @@ const ProductTemplateEdit = (
}
// If `inherit` is truthy, adjust conditionally the query to create a better preview.
if ( inherit ) {
- if ( templateCategory ) {
- query.categories = templateCategory[ 0 ]?.id;
+ const { taxonomy, slug } = parseTemplateSlug( templateSlug );
+
+ if ( taxonomy && slug ) {
+ const taxonomyRecord = getEntityRecords(
+ 'taxonomy',
+ taxonomy,
+ {
+ context: 'view',
+ per_page: 1,
+ _fields: [ 'id' ],
+ slug,
+ }
+ );
+
+ if ( taxonomyRecord ) {
+ const taxonomyId = taxonomyRecord[ 0 ]?.id;
+ if ( taxonomy === 'category' ) {
+ query.categories = taxonomyId;
+ } else {
+ // If taxonomy is not `category`, we expect either `product_cat` or `product_tag`
+ query[ taxonomy ] = taxonomyId;
+ }
+ }
}
query.per_page = loopShopPerPage;
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-template/utils.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-template/utils.tsx
index b6d46287cd2..5f5344a7296 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-template/utils.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-template/utils.tsx
@@ -366,3 +366,32 @@ export const useProductCollectionQueryContext = ( {
return queryContext;
}, [ queryContextIncludes, productCollectionBlockAttributes ] );
};
+
+export const parseTemplateSlug = ( rawTemplateSlug = '' ) => {
+ const categoryPrefix = 'category-';
+ const productCategoryPrefix = 'taxonomy-product_cat-';
+ const productTagPrefix = 'taxonomy-product_tag-';
+
+ if ( rawTemplateSlug.startsWith( categoryPrefix ) ) {
+ return {
+ taxonomy: 'category',
+ slug: rawTemplateSlug.replace( categoryPrefix, '' ),
+ };
+ }
+
+ if ( rawTemplateSlug.startsWith( productCategoryPrefix ) ) {
+ return {
+ taxonomy: 'product_cat',
+ slug: rawTemplateSlug.replace( productCategoryPrefix, '' ),
+ };
+ }
+
+ if ( rawTemplateSlug.startsWith( productTagPrefix ) ) {
+ return {
+ taxonomy: 'product_tag',
+ slug: rawTemplateSlug.replace( productTagPrefix, '' ),
+ };
+ }
+
+ return { taxonomy: '', slug: '' };
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/products-by-attribute/inspector-controls.tsx b/plugins/woocommerce-blocks/assets/js/blocks/products-by-attribute/inspector-controls.tsx
index a700cd56aae..06863da31e2 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/products-by-attribute/inspector-controls.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/products-by-attribute/inspector-controls.tsx
@@ -61,7 +61,7 @@ export const ProductsByAttributeInspectorControls = (
selected={ attributes }
onChange={ ( value = [] ) => {
const result = value.map(
- ( { id, attr_slug: attributeSlug } ) => ( {
+ ( { id, value: attributeSlug } ) => ( {
id,
attr_slug: attributeSlug,
} )
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/rating-filter/test/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/rating-filter/test/block.tsx
index 1cfbe910fb7..aad8468ae82 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/rating-filter/test/block.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/rating-filter/test/block.tsx
@@ -2,7 +2,14 @@
* External dependencies
*/
import React from '@wordpress/element';
-import { render, screen, waitFor, within } from '@testing-library/react';
+import {
+ act,
+ cleanup,
+ render,
+ screen,
+ waitFor,
+ within,
+} from '@testing-library/react';
import * as hooks from '@woocommerce/base-context/hooks';
import userEvent from '@testing-library/user-event';
@@ -59,6 +66,7 @@ const selectors = {
};
const setup = ( params: SetupParams ) => {
+ cleanup();
const url = `http://woo.local/${
params.filterRating ? '?rating_filter=' + params.filterRating : ''
}`;
@@ -78,14 +86,7 @@ const setup = ( params: SetupParams ) => {
} );
const { container, ...utils } = render(
- ,
- { legacyRoot: true }
- );
-
- // We need to switch to React 17 rendering to allow these tests to keep passing, but as a result the React
- // rendering error will be shown.
- expect( console ).toHaveErroredWith(
- `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot`
+
);
const getList = () => container.querySelector( selectors.list );
@@ -203,74 +204,81 @@ describe( 'Filter by Rating block', () => {
describe( 'Single choice Dropdown', () => {
test( 'renders dropdown', () => {
const { getDropdown, getList } = setupSingleChoiceDropdown();
+
expect( getDropdown() ).toBeInTheDocument();
expect( getList() ).toBeNull();
} );
- test( 'renders chips based on URL params', () => {
- const ratingParam = '2';
- const { getRating2Chips, getRating4Chips, getRating5Chips } =
- setupSingleChoiceDropdown( ratingParam );
+ test( 'renders chips based on URL params', async () => {
+ await waitFor( async () => {
+ const ratingParam = '2';
+ const { getRating2Chips, getRating4Chips, getRating5Chips } =
+ setupSingleChoiceDropdown( ratingParam );
- expect( getRating2Chips() ).toBeInTheDocument();
- expect( getRating4Chips() ).toBeNull();
- expect( getRating5Chips() ).toBeNull();
+ expect( getRating2Chips() ).toBeInTheDocument();
+ expect( getRating4Chips() ).toBeNull();
+ expect( getRating5Chips() ).toBeNull();
+ } );
} );
test( 'replaces chosen option when another one is clicked', async () => {
- const ratingParam = '2';
- const {
- getDropdown,
- getRating2Chips,
- getRating4Chips,
- getRating4Suggestion,
- } = setupSingleChoiceDropdown( ratingParam );
+ await waitFor( async () => {
+ const ratingParam = '2';
+ const {
+ getDropdown,
+ getRating2Chips,
+ getRating4Chips,
+ getRating4Suggestion,
+ } = setupSingleChoiceDropdown( ratingParam );
- expect( getRating2Chips() ).toBeInTheDocument();
- expect( getRating4Chips() ).toBeNull();
+ expect( getRating2Chips() ).toBeInTheDocument();
+ expect( getRating4Chips() ).toBeNull();
- const dropdown = getDropdown();
+ const dropdown = getDropdown();
- if ( dropdown ) {
- await userEvent.click( dropdown );
- acceptErrorWithDuplicatedKeys();
- }
+ if ( dropdown ) {
+ await userEvent.click( dropdown );
+ acceptErrorWithDuplicatedKeys();
+ }
- const rating4Suggestion = getRating4Suggestion();
+ const rating4Suggestion = getRating4Suggestion();
- if ( rating4Suggestion ) {
- await userEvent.click( rating4Suggestion );
- }
+ if ( rating4Suggestion ) {
+ await userEvent.click( rating4Suggestion );
+ }
- expect( getRating2Chips() ).toBeNull();
- expect( getRating4Chips() ).toBeInTheDocument();
+ expect( getRating2Chips() ).toBeNull();
+ expect( getRating4Chips() ).toBeInTheDocument();
+ } );
} );
test( 'removes the option when the X button is clicked', async () => {
- const ratingParam = '4';
- const {
- getRating2Chips,
- getRating4Chips,
- getRating5Chips,
- getRemoveButtonFromChips,
- } = setupMultipleChoiceDropdown( ratingParam );
+ await waitFor( async () => {
+ const ratingParam = '4';
+ const {
+ getRating2Chips,
+ getRating4Chips,
+ getRating5Chips,
+ getRemoveButtonFromChips,
+ } = setupMultipleChoiceDropdown( ratingParam );
- expect( getRating2Chips() ).toBeNull();
- expect( getRating4Chips() ).toBeInTheDocument();
- expect( getRating5Chips() ).toBeNull();
+ expect( getRating2Chips() ).toBeNull();
+ expect( getRating4Chips() ).toBeInTheDocument();
+ expect( getRating5Chips() ).toBeNull();
- const removeRating4Button = getRemoveButtonFromChips(
- getRating4Chips()
- );
+ const removeRating4Button = getRemoveButtonFromChips(
+ getRating4Chips()
+ );
- if ( removeRating4Button ) {
- await userEvent.click( removeRating4Button );
- acceptErrorWithDuplicatedKeys();
- }
+ if ( removeRating4Button ) {
+ await userEvent.click( removeRating4Button );
+ acceptErrorWithDuplicatedKeys();
+ }
- expect( getRating2Chips() ).toBeNull();
- expect( getRating4Chips() ).toBeNull();
- expect( getRating5Chips() ).toBeNull();
+ expect( getRating2Chips() ).toBeNull();
+ expect( getRating4Chips() ).toBeNull();
+ expect( getRating5Chips() ).toBeNull();
+ } );
} );
} );
@@ -281,83 +289,89 @@ describe( 'Filter by Rating block', () => {
expect( getList() ).toBeNull();
} );
- test( 'renders chips based on URL params', () => {
- const ratingParam = '2,4';
- const { getRating2Chips, getRating4Chips, getRating5Chips } =
- setupMultipleChoiceDropdown( ratingParam );
+ test( 'renders chips based on URL params', async () => {
+ await waitFor( async () => {
+ const ratingParam = '2,4';
+ const { getRating2Chips, getRating4Chips, getRating5Chips } =
+ setupMultipleChoiceDropdown( ratingParam );
- expect( getRating2Chips() ).toBeInTheDocument();
- expect( getRating4Chips() ).toBeInTheDocument();
- expect( getRating5Chips() ).toBeNull();
+ expect( getRating2Chips() ).toBeInTheDocument();
+ expect( getRating4Chips() ).toBeInTheDocument();
+ expect( getRating5Chips() ).toBeNull();
+ } );
} );
test( 'adds chosen option to another one that is clicked', async () => {
- const ratingParam = '2';
- const {
- getDropdown,
- getRating2Chips,
- getRating4Chips,
- getRating5Chips,
- getRating4Suggestion,
- getRating5Suggestion,
- } = setupMultipleChoiceDropdown( ratingParam );
+ await waitFor( async () => {
+ const ratingParam = '2';
+ const {
+ getDropdown,
+ getRating2Chips,
+ getRating4Chips,
+ getRating5Chips,
+ getRating4Suggestion,
+ getRating5Suggestion,
+ } = setupMultipleChoiceDropdown( ratingParam );
- expect( getRating2Chips() ).toBeInTheDocument();
- expect( getRating4Chips() ).toBeNull();
- expect( getRating5Chips() ).toBeNull();
+ expect( getRating2Chips() ).toBeInTheDocument();
+ expect( getRating4Chips() ).toBeNull();
+ expect( getRating5Chips() ).toBeNull();
- const dropdown = getDropdown();
+ const dropdown = getDropdown();
- if ( dropdown ) {
- await userEvent.click( dropdown );
- acceptErrorWithDuplicatedKeys();
- }
+ if ( dropdown ) {
+ await userEvent.click( dropdown );
+ acceptErrorWithDuplicatedKeys();
+ }
- const rating4Suggestion = getRating4Suggestion();
+ const rating4Suggestion = getRating4Suggestion();
- if ( rating4Suggestion ) {
- await userEvent.click( rating4Suggestion );
- }
+ if ( rating4Suggestion ) {
+ await userEvent.click( rating4Suggestion );
+ }
- expect( getRating2Chips() ).toBeInTheDocument();
- expect( getRating4Chips() ).toBeInTheDocument();
- expect( getRating5Chips() ).toBeNull();
+ expect( getRating2Chips() ).toBeInTheDocument();
+ expect( getRating4Chips() ).toBeInTheDocument();
+ expect( getRating5Chips() ).toBeNull();
- const rating5Suggestion = getRating5Suggestion();
+ const rating5Suggestion = getRating5Suggestion();
- if ( rating5Suggestion ) {
- await userEvent.click( rating5Suggestion );
- }
+ if ( rating5Suggestion ) {
+ await userEvent.click( rating5Suggestion );
+ }
- expect( getRating2Chips() ).toBeInTheDocument();
- expect( getRating4Chips() ).toBeInTheDocument();
- expect( getRating5Chips() ).toBeInTheDocument();
+ expect( getRating2Chips() ).toBeInTheDocument();
+ expect( getRating4Chips() ).toBeInTheDocument();
+ expect( getRating5Chips() ).toBeInTheDocument();
+ } );
} );
test( 'removes the option when the X button is clicked', async () => {
- const ratingParam = '2,4,5';
- const {
- getRating2Chips,
- getRating4Chips,
- getRating5Chips,
- getRemoveButtonFromChips,
- } = setupMultipleChoiceDropdown( ratingParam );
+ await waitFor( async () => {
+ const ratingParam = '2,4,5';
+ const {
+ getRating2Chips,
+ getRating4Chips,
+ getRating5Chips,
+ getRemoveButtonFromChips,
+ } = setupMultipleChoiceDropdown( ratingParam );
- expect( getRating2Chips() ).toBeInTheDocument();
- expect( getRating4Chips() ).toBeInTheDocument();
- expect( getRating5Chips() ).toBeInTheDocument();
+ expect( getRating2Chips() ).toBeInTheDocument();
+ expect( getRating4Chips() ).toBeInTheDocument();
+ expect( getRating5Chips() ).toBeInTheDocument();
- const removeRating4Button = getRemoveButtonFromChips(
- getRating4Chips()
- );
+ const removeRating4Button = getRemoveButtonFromChips(
+ getRating4Chips()
+ );
- if ( removeRating4Button ) {
- await userEvent.click( removeRating4Button );
- }
+ if ( removeRating4Button ) {
+ await userEvent.click( removeRating4Button );
+ }
- expect( getRating2Chips() ).toBeInTheDocument();
- expect( getRating4Chips() ).toBeNull();
- expect( getRating5Chips() ).toBeInTheDocument();
+ expect( getRating2Chips() ).toBeInTheDocument();
+ expect( getRating4Chips() ).toBeNull();
+ expect( getRating5Chips() ).toBeInTheDocument();
+ } );
} );
} );
@@ -368,61 +382,67 @@ describe( 'Filter by Rating block', () => {
expect( getList() ).toBeInTheDocument();
} );
- test( 'renders checked options based on URL params', () => {
- const ratingParam = '4';
- const {
- getRating2Checkbox,
- getRating4Checkbox,
- getRating5Checkbox,
- } = setupSingleChoiceList( ratingParam );
+ test( 'renders checked options based on URL params', async () => {
+ await waitFor( async () => {
+ const ratingParam = '4';
+ const {
+ getRating2Checkbox,
+ getRating4Checkbox,
+ getRating5Checkbox,
+ } = setupSingleChoiceList( ratingParam );
- expect( getRating2Checkbox()?.checked ).toBeFalsy();
- expect( getRating4Checkbox()?.checked ).toBeTruthy();
- expect( getRating5Checkbox()?.checked ).toBeFalsy();
+ expect( getRating2Checkbox()?.checked ).toBeFalsy();
+ expect( getRating4Checkbox()?.checked ).toBeTruthy();
+ expect( getRating5Checkbox()?.checked ).toBeFalsy();
+ } );
} );
test( 'replaces chosen option when another one is clicked', async () => {
- const ratingParam = '2';
- const {
- getRating2Checkbox,
- getRating4Checkbox,
- getRating5Checkbox,
- } = setupSingleChoiceList( ratingParam );
+ await waitFor( async () => {
+ const ratingParam = '2';
+ const {
+ getRating2Checkbox,
+ getRating4Checkbox,
+ getRating5Checkbox,
+ } = setupSingleChoiceList( ratingParam );
- expect( getRating2Checkbox()?.checked ).toBeTruthy();
- expect( getRating4Checkbox()?.checked ).toBeFalsy();
- expect( getRating5Checkbox()?.checked ).toBeFalsy();
+ expect( getRating2Checkbox()?.checked ).toBeTruthy();
+ expect( getRating4Checkbox()?.checked ).toBeFalsy();
+ expect( getRating5Checkbox()?.checked ).toBeFalsy();
- const rating4checkbox = getRating4Checkbox();
+ const rating4checkbox = getRating4Checkbox();
- if ( rating4checkbox ) {
- await userEvent.click( rating4checkbox );
- }
+ if ( rating4checkbox ) {
+ await act( async () => {
+ await userEvent.click( rating4checkbox );
+ } );
+ }
- expect( getRating2Checkbox()?.checked ).toBeFalsy();
- expect( getRating4Checkbox()?.checked ).toBeTruthy();
- expect( getRating5Checkbox()?.checked ).toBeFalsy();
+ expect( getRating2Checkbox()?.checked ).toBeFalsy();
+ expect( getRating4Checkbox()?.checked ).toBeTruthy();
+ expect( getRating5Checkbox()?.checked ).toBeFalsy();
+ } );
} );
test( 'removes the option when it is clicked again', async () => {
- const ratingParam = '4';
- const {
- getRating2Checkbox,
- getRating4Checkbox,
- getRating5Checkbox,
- } = setupMultipleChoiceList( ratingParam );
+ await waitFor( async () => {
+ const ratingParam = '4';
+ const {
+ getRating2Checkbox,
+ getRating4Checkbox,
+ getRating5Checkbox,
+ } = setupMultipleChoiceList( ratingParam );
- expect( getRating2Checkbox()?.checked ).toBeFalsy();
- expect( getRating4Checkbox()?.checked ).toBeTruthy();
- expect( getRating5Checkbox()?.checked ).toBeFalsy();
+ expect( getRating2Checkbox()?.checked ).toBeFalsy();
+ expect( getRating4Checkbox()?.checked ).toBeTruthy();
+ expect( getRating5Checkbox()?.checked ).toBeFalsy();
- const rating4checkbox = getRating4Checkbox();
+ const rating4checkbox = getRating4Checkbox();
- if ( rating4checkbox ) {
- await userEvent.click( rating4checkbox );
- }
+ if ( rating4checkbox ) {
+ await userEvent.click( rating4checkbox );
+ }
- await waitFor( () => {
expect( getRating2Checkbox()?.checked ).toBeFalsy();
expect( getRating4Checkbox()?.checked ).toBeFalsy();
expect( getRating5Checkbox()?.checked ).toBeFalsy();
@@ -437,38 +457,40 @@ describe( 'Filter by Rating block', () => {
expect( getList() ).toBeInTheDocument();
} );
- test( 'renders chips based on URL params', () => {
- const ratingParam = '4,5';
- const {
- getRating2Checkbox,
- getRating4Checkbox,
- getRating5Checkbox,
- } = setupMultipleChoiceList( ratingParam );
+ test( 'renders chips based on URL params', async () => {
+ await waitFor( async () => {
+ const ratingParam = '4,5';
+ const {
+ getRating2Checkbox,
+ getRating4Checkbox,
+ getRating5Checkbox,
+ } = setupMultipleChoiceList( ratingParam );
- expect( getRating2Checkbox()?.checked ).toBeFalsy();
- expect( getRating4Checkbox()?.checked ).toBeTruthy();
- expect( getRating5Checkbox()?.checked ).toBeTruthy();
+ expect( getRating2Checkbox()?.checked ).toBeFalsy();
+ expect( getRating4Checkbox()?.checked ).toBeTruthy();
+ expect( getRating5Checkbox()?.checked ).toBeTruthy();
+ } );
} );
test( 'adds chosen option to another one that is clicked', async () => {
- const ratingParam = '2,4';
- const {
- getRating2Checkbox,
- getRating4Checkbox,
- getRating5Checkbox,
- } = setupMultipleChoiceList( ratingParam );
+ await waitFor( async () => {
+ const ratingParam = '2,4';
+ const {
+ getRating2Checkbox,
+ getRating4Checkbox,
+ getRating5Checkbox,
+ } = setupMultipleChoiceList( ratingParam );
- expect( getRating2Checkbox()?.checked ).toBeTruthy();
- expect( getRating4Checkbox()?.checked ).toBeTruthy();
- expect( getRating5Checkbox()?.checked ).toBeFalsy();
+ expect( getRating2Checkbox()?.checked ).toBeTruthy();
+ expect( getRating4Checkbox()?.checked ).toBeTruthy();
+ expect( getRating5Checkbox()?.checked ).toBeFalsy();
- const rating5checkbox = getRating5Checkbox();
+ const rating5checkbox = getRating5Checkbox();
- if ( rating5checkbox ) {
- await userEvent.click( rating5checkbox );
- }
+ if ( rating5checkbox ) {
+ await userEvent.click( rating5checkbox );
+ }
- await waitFor( () => {
expect( getRating2Checkbox()?.checked ).toBeTruthy();
expect( getRating4Checkbox()?.checked ).toBeTruthy();
expect( getRating5Checkbox()?.checked ).toBeTruthy();
@@ -476,24 +498,24 @@ describe( 'Filter by Rating block', () => {
} );
test( 'removes the option when it is clicked again', async () => {
- const ratingParam = '2,4';
- const {
- getRating2Checkbox,
- getRating4Checkbox,
- getRating5Checkbox,
- } = setupMultipleChoiceList( ratingParam );
+ await waitFor( async () => {
+ const ratingParam = '2,4';
+ const {
+ getRating2Checkbox,
+ getRating4Checkbox,
+ getRating5Checkbox,
+ } = setupMultipleChoiceList( ratingParam );
- expect( getRating2Checkbox()?.checked ).toBeTruthy();
- expect( getRating4Checkbox()?.checked ).toBeTruthy();
- expect( getRating5Checkbox()?.checked ).toBeFalsy();
+ expect( getRating2Checkbox()?.checked ).toBeTruthy();
+ expect( getRating4Checkbox()?.checked ).toBeTruthy();
+ expect( getRating5Checkbox()?.checked ).toBeFalsy();
- const rating2checkbox = getRating2Checkbox();
+ const rating2checkbox = getRating2Checkbox();
- if ( rating2checkbox ) {
- await userEvent.click( rating2checkbox );
- }
+ if ( rating2checkbox ) {
+ await userEvent.click( rating2checkbox );
+ }
- await waitFor( () => {
expect( getRating2Checkbox()?.checked ).toBeFalsy();
expect( getRating4Checkbox()?.checked ).toBeTruthy();
expect( getRating5Checkbox()?.checked ).toBeFalsy();
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/stock-filter/test/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/stock-filter/test/block.tsx
index 358b619b96d..be9786cb8f5 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/stock-filter/test/block.tsx
+++ b/plugins/woocommerce-blocks/assets/js/blocks/stock-filter/test/block.tsx
@@ -2,7 +2,14 @@
* External dependencies
*/
import React from '@wordpress/element';
-import { act, render, screen, within, waitFor } from '@testing-library/react';
+import {
+ act,
+ cleanup,
+ render,
+ screen,
+ within,
+ waitFor,
+} from '@testing-library/react';
import { default as fetchMock } from 'jest-fetch-mock';
import userEvent from '@testing-library/user-event';
@@ -68,6 +75,7 @@ const selectors = {
};
const setup = ( params: SetupParams = {} ) => {
+ cleanup();
const url = `http://woo.local/${
params.filterStock ? '?filter_stock_status=' + params.filterStock : ''
}`;
@@ -87,14 +95,7 @@ const setup = ( params: SetupParams = {} ) => {
};
const { container, ...utils } = render(
- ,
- { legacyRoot: true }
- );
-
- // We need to switch to React 17 rendering to allow these tests to keep passing, but as a result the React
- // rendering error will be shown.
- expect( console ).toHaveErroredWith(
- `Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot`
+
);
const getList = () => container.querySelector( selectors.list );
@@ -227,7 +228,7 @@ describe( 'Filter by Stock block', () => {
fetchMock.resetMocks();
} );
- it( 'renders the stock filter block', async () => {
+ test( 'renders the stock filter block', async () => {
const { container } = setup( {
showFilterButton: false,
showCounts: false,
@@ -235,7 +236,7 @@ describe( 'Filter by Stock block', () => {
expect( container ).toMatchSnapshot();
} );
- it( 'renders the stock filter block with the filter button', async () => {
+ test( 'renders the stock filter block with the filter button', async () => {
const { container } = setup( {
showFilterButton: true,
showCounts: false,
@@ -243,7 +244,7 @@ describe( 'Filter by Stock block', () => {
expect( container ).toMatchSnapshot();
} );
- it( 'renders the stock filter block with the product counts', async () => {
+ test( 'renders the stock filter block with the product counts', async () => {
const { container } = setup( {
showFilterButton: false,
showCounts: true,
@@ -254,80 +255,86 @@ describe( 'Filter by Stock block', () => {
describe( 'Single choice Dropdown', () => {
test( 'renders dropdown', () => {
const { getDropdown, getList } = setupSingleChoiceDropdown();
+
expect( getDropdown() ).toBeInTheDocument();
expect( getList() ).toBeNull();
} );
- test( 'renders chips based on URL params', () => {
- const ratingParam = 'instock';
- const { getInStockChips, getOutOfStockChips, getOnBackorderChips } =
- setupSingleChoiceDropdown( ratingParam );
+ test( 'renders chips based on URL params', async () => {
+ await waitFor( async () => {
+ const ratingParam = 'instock';
+ const {
+ getInStockChips,
+ getOutOfStockChips,
+ getOnBackorderChips,
+ } = setupSingleChoiceDropdown( ratingParam );
- expect( getInStockChips() ).toBeInTheDocument();
- expect( getOutOfStockChips() ).toBeNull();
- expect( getOnBackorderChips() ).toBeNull();
+ expect( getInStockChips() ).toBeInTheDocument();
+ expect( getOutOfStockChips() ).toBeNull();
+ expect( getOnBackorderChips() ).toBeNull();
+ } );
} );
test( 'replaces chosen option when another one is clicked', async () => {
- const user = userEvent.setup();
- const ratingParam = 'instock';
- const {
- getDropdown,
- getInStockChips,
- getOutOfStockChips,
- getOutOfStockSuggestion,
- } = setupSingleChoiceDropdown( ratingParam );
+ await waitFor( async () => {
+ const user = userEvent.setup();
+ const ratingParam = 'instock';
+ const {
+ getDropdown,
+ getInStockChips,
+ getOutOfStockChips,
+ getOutOfStockSuggestion,
+ } = setupSingleChoiceDropdown( ratingParam );
- expect( getInStockChips() ).toBeInTheDocument();
- expect( getOutOfStockChips() ).toBeNull();
+ expect( getInStockChips() ).toBeInTheDocument();
+ expect( getOutOfStockChips() ).toBeNull();
- const dropdown = getDropdown();
+ const dropdown = getDropdown();
- if ( dropdown ) {
- await act( async () => {
- await user.click( dropdown );
- } );
- }
+ if ( dropdown ) {
+ await act( async () => {
+ await user.click( dropdown );
+ } );
+ }
- const outOfStockSuggestion = getOutOfStockSuggestion();
+ const outOfStockSuggestion = getOutOfStockSuggestion();
- if ( outOfStockSuggestion ) {
- await act( async () => {
- await user.click( outOfStockSuggestion );
- } );
- }
+ if ( outOfStockSuggestion ) {
+ await act( async () => {
+ await user.click( outOfStockSuggestion );
+ } );
+ }
- expect( getInStockChips() ).toBeNull();
- expect( getOutOfStockChips() ).toBeInTheDocument();
+ expect( getInStockChips() ).toBeNull();
+ expect( getOutOfStockChips() ).toBeInTheDocument();
+ } );
} );
test( 'removes the option when the X button is clicked', async () => {
- const user = userEvent.setup();
- const ratingParam = 'outofstock';
- const {
- getInStockChips,
- getOutOfStockChips,
- getOnBackorderChips,
- getRemoveButtonFromChips,
- } = setupMultipleChoiceDropdown( ratingParam );
+ await waitFor( async () => {
+ const user = userEvent.setup();
+ const ratingParam = 'outofstock';
+ const {
+ getInStockChips,
+ getOutOfStockChips,
+ getOnBackorderChips,
+ getRemoveButtonFromChips,
+ } = setupMultipleChoiceDropdown( ratingParam );
- await waitFor( () => {
expect( getInStockChips() ).toBeNull();
expect( getOutOfStockChips() ).toBeInTheDocument();
expect( getOnBackorderChips() ).toBeNull();
- } );
- const removeOutOfStockButton = getRemoveButtonFromChips(
- getOutOfStockChips()
- );
+ const removeOutOfStockButton = getRemoveButtonFromChips(
+ getOutOfStockChips()
+ );
- if ( removeOutOfStockButton ) {
- act( async () => {
- await user.click( removeOutOfStockButton );
- } );
- }
+ if ( removeOutOfStockButton ) {
+ await act( async () => {
+ await user.click( removeOutOfStockButton );
+ } );
+ }
- await waitFor( () => {
expect( getInStockChips() ).toBeNull();
expect( getOutOfStockChips() ).toBeNull();
expect( getOnBackorderChips() ).toBeNull();
@@ -342,67 +349,65 @@ describe( 'Filter by Stock block', () => {
expect( getList() ).toBeNull();
} );
- test( 'renders chips based on URL params', () => {
- const ratingParam = 'instock,onbackorder';
- const { getInStockChips, getOutOfStockChips, getOnBackorderChips } =
- setupMultipleChoiceDropdown( ratingParam );
+ test( 'renders chips based on URL params', async () => {
+ await waitFor( async () => {
+ const ratingParam = 'instock,onbackorder';
+ const {
+ getInStockChips,
+ getOutOfStockChips,
+ getOnBackorderChips,
+ } = setupMultipleChoiceDropdown( ratingParam );
- expect( getInStockChips() ).toBeInTheDocument();
- expect( getOutOfStockChips() ).toBeNull();
- expect( getOnBackorderChips() ).toBeInTheDocument();
- } );
-
- test( 'adds chosen option to another one that is clicked', async () => {
- const user = userEvent.setup();
- const ratingParam = 'onbackorder';
- const {
- getDropdown,
- getInStockChips,
- getOutOfStockChips,
- getOnBackorderChips,
- getInStockSuggestion,
- getOutOfStockSuggestion,
- } = setupMultipleChoiceDropdown( ratingParam );
-
- await waitFor( () => {
- expect( getInStockChips() ).toBeNull();
+ expect( getInStockChips() ).toBeInTheDocument();
expect( getOutOfStockChips() ).toBeNull();
expect( getOnBackorderChips() ).toBeInTheDocument();
} );
- const dropdown = getDropdown();
+ } );
- if ( dropdown ) {
- await act( async () => {
+ test( 'adds chosen option to another one that is clicked', async () => {
+ await waitFor( async () => {
+ const user = userEvent.setup();
+ const ratingParam = 'onbackorder';
+ const {
+ getDropdown,
+ getInStockChips,
+ getOutOfStockChips,
+ getOnBackorderChips,
+ getInStockSuggestion,
+ getOutOfStockSuggestion,
+ } = setupMultipleChoiceDropdown( ratingParam );
+
+ expect( getInStockChips() ).toBeNull();
+ expect( getOutOfStockChips() ).toBeNull();
+ expect( getOnBackorderChips() ).toBeInTheDocument();
+
+ const dropdown = getDropdown();
+
+ if ( dropdown ) {
await user.click( dropdown );
- } );
- }
+ }
- const inStockSuggestion = getInStockSuggestion();
+ const inStockSuggestion = getInStockSuggestion();
- if ( inStockSuggestion ) {
- await act( async () => {
+ if ( inStockSuggestion ) {
await user.click( inStockSuggestion );
- } );
- }
+ }
- expect( getInStockChips() ).toBeInTheDocument();
- expect( getOutOfStockChips() ).toBeNull();
- expect( getOnBackorderChips() ).toBeInTheDocument();
+ expect( getInStockChips() ).toBeInTheDocument();
+ expect( getOutOfStockChips() ).toBeNull();
+ expect( getOnBackorderChips() ).toBeInTheDocument();
- const freshDropdown = getDropdown();
- if ( freshDropdown ) {
- await act( async () => {
+ const freshDropdown = getDropdown();
+ if ( freshDropdown ) {
await user.click( freshDropdown );
- } );
- }
+ }
- const outOfStockSuggestion = getOutOfStockSuggestion();
+ const outOfStockSuggestion = getOutOfStockSuggestion();
- if ( outOfStockSuggestion ) {
- userEvent.click( outOfStockSuggestion );
- }
+ if ( outOfStockSuggestion ) {
+ await userEvent.click( outOfStockSuggestion );
+ }
- await waitFor( () => {
expect( getInStockChips() ).toBeInTheDocument();
expect( getOutOfStockChips() ).toBeInTheDocument();
expect( getOnBackorderChips() ).toBeInTheDocument();
@@ -410,32 +415,30 @@ describe( 'Filter by Stock block', () => {
} );
test( 'removes the option when the X button is clicked', async () => {
- const user = userEvent.setup();
- const ratingParam = 'instock,outofstock,onbackorder';
- const {
- getInStockChips,
- getOutOfStockChips,
- getOnBackorderChips,
- getRemoveButtonFromChips,
- } = setupMultipleChoiceDropdown( ratingParam );
+ await waitFor( async () => {
+ const user = userEvent.setup();
+ const ratingParam = 'instock,outofstock,onbackorder';
+ const {
+ getInStockChips,
+ getOutOfStockChips,
+ getOnBackorderChips,
+ getRemoveButtonFromChips,
+ } = setupMultipleChoiceDropdown( ratingParam );
- await waitFor( () => {
expect( getInStockChips() ).toBeInTheDocument();
expect( getOutOfStockChips() ).toBeInTheDocument();
expect( getOnBackorderChips() ).toBeInTheDocument();
- } );
- const removeOutOfStockButton = getRemoveButtonFromChips(
- getOutOfStockChips()
- );
+ const removeOutOfStockButton = getRemoveButtonFromChips(
+ getOutOfStockChips()
+ );
- if ( removeOutOfStockButton ) {
- act( async () => {
- await user.click( removeOutOfStockButton );
- } );
- }
+ if ( removeOutOfStockButton ) {
+ await act( async () => {
+ await user.click( removeOutOfStockButton );
+ } );
+ }
- await waitFor( () => {
expect( getInStockChips() ).toBeInTheDocument();
expect( getOutOfStockChips() ).toBeNull();
expect( getOnBackorderChips() ).toBeInTheDocument();
@@ -450,67 +453,73 @@ describe( 'Filter by Stock block', () => {
expect( getList() ).toBeInTheDocument();
} );
- test( 'renders checked options based on URL params', () => {
- const ratingParam = 'instock';
- const {
- getInStockCheckbox,
- getOutOfStockCheckbox,
- getOnBackorderCheckbox,
- } = setupSingleChoiceList( ratingParam );
+ test( 'renders checked options based on URL params', async () => {
+ await waitFor( async () => {
+ const ratingParam = 'instock';
+ const {
+ getInStockCheckbox,
+ getOutOfStockCheckbox,
+ getOnBackorderCheckbox,
+ } = setupSingleChoiceList( ratingParam );
- expect( getInStockCheckbox()?.checked ).toBeTruthy();
- expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
- expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
+ expect( getInStockCheckbox()?.checked ).toBeTruthy();
+ expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
+ expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
+ } );
} );
test( 'replaces chosen option when another one is clicked', async () => {
- const user = userEvent.setup();
- const ratingParam = 'outofstock';
- const {
- getInStockCheckbox,
- getOutOfStockCheckbox,
- getOnBackorderCheckbox,
- } = setupSingleChoiceList( ratingParam );
+ await waitFor( async () => {
+ const user = userEvent.setup();
+ const ratingParam = 'outofstock';
+ const {
+ getInStockCheckbox,
+ getOutOfStockCheckbox,
+ getOnBackorderCheckbox,
+ } = setupSingleChoiceList( ratingParam );
- expect( getInStockCheckbox()?.checked ).toBeFalsy();
- expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
- expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
+ expect( getInStockCheckbox()?.checked ).toBeFalsy();
+ expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
+ expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
- const onBackorderCheckbox = getOnBackorderCheckbox();
+ const onBackorderCheckbox = getOnBackorderCheckbox();
- if ( onBackorderCheckbox ) {
- await act( async () => {
- await user.click( onBackorderCheckbox );
- } );
- }
+ if ( onBackorderCheckbox ) {
+ await act( async () => {
+ await user.click( onBackorderCheckbox );
+ } );
+ }
- expect( getInStockCheckbox()?.checked ).toBeFalsy();
- expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
- expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
+ expect( getInStockCheckbox()?.checked ).toBeFalsy();
+ expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
+ expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
+ } );
} );
test( 'removes the option when it is clicked again', async () => {
- const ratingParam = 'onbackorder';
- const {
- getInStockCheckbox,
- getOutOfStockCheckbox,
- getOnBackorderCheckbox,
- } = setupMultipleChoiceList( ratingParam );
+ await waitFor( async () => {
+ const ratingParam = 'onbackorder';
+ const {
+ getInStockCheckbox,
+ getOutOfStockCheckbox,
+ getOnBackorderCheckbox,
+ } = setupMultipleChoiceList( ratingParam );
- expect( getInStockCheckbox()?.checked ).toBeFalsy();
- expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
- expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
-
- const onBackorderCheckbox = getOnBackorderCheckbox();
-
- if ( onBackorderCheckbox ) {
- userEvent.click( onBackorderCheckbox );
- }
-
- await waitFor( () => {
expect( getInStockCheckbox()?.checked ).toBeFalsy();
expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
- expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
+ expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
+
+ const onBackorderCheckbox = getOnBackorderCheckbox();
+
+ if ( onBackorderCheckbox ) {
+ userEvent.click( onBackorderCheckbox );
+ }
+
+ await waitFor( () => {
+ expect( getInStockCheckbox()?.checked ).toBeFalsy();
+ expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
+ expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
+ } );
} );
} );
} );
@@ -522,66 +531,72 @@ describe( 'Filter by Stock block', () => {
expect( getList() ).toBeInTheDocument();
} );
- test( 'renders chips based on URL params', () => {
- const ratingParam = 'instock,onbackorder';
- const {
- getInStockCheckbox,
- getOutOfStockCheckbox,
- getOnBackorderCheckbox,
- } = setupMultipleChoiceList( ratingParam );
+ test( 'renders chips based on URL params', async () => {
+ await waitFor( async () => {
+ const ratingParam = 'instock,onbackorder';
+ const {
+ getInStockCheckbox,
+ getOutOfStockCheckbox,
+ getOnBackorderCheckbox,
+ } = setupMultipleChoiceList( ratingParam );
- expect( getInStockCheckbox()?.checked ).toBeTruthy();
- expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
- expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
- } );
-
- test( 'adds chosen option to another one that is clicked', async () => {
- const ratingParam = 'outofstock,onbackorder';
- const {
- getInStockCheckbox,
- getOutOfStockCheckbox,
- getOnBackorderCheckbox,
- } = setupMultipleChoiceList( ratingParam );
-
- expect( getInStockCheckbox()?.checked ).toBeFalsy();
- expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
- expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
-
- const inStockCheckbox = getInStockCheckbox();
-
- if ( inStockCheckbox ) {
- userEvent.click( inStockCheckbox );
- }
-
- await waitFor( () => {
expect( getInStockCheckbox()?.checked ).toBeTruthy();
- expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
+ expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
} );
} );
- test( 'removes the option when it is clicked again', async () => {
- const ratingParam = 'instock,outofstock';
- const {
- getInStockCheckbox,
- getOutOfStockCheckbox,
- getOnBackorderCheckbox,
- } = setupMultipleChoiceList( ratingParam );
+ test( 'adds chosen option to another one that is clicked', async () => {
+ await waitFor( async () => {
+ const ratingParam = 'outofstock,onbackorder';
+ const {
+ getInStockCheckbox,
+ getOutOfStockCheckbox,
+ getOnBackorderCheckbox,
+ } = setupMultipleChoiceList( ratingParam );
- expect( getInStockCheckbox()?.checked ).toBeTruthy();
- expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
- expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
-
- const inStockCheckbox = getInStockCheckbox();
-
- if ( inStockCheckbox ) {
- userEvent.click( inStockCheckbox );
- }
-
- await waitFor( () => {
expect( getInStockCheckbox()?.checked ).toBeFalsy();
expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
+ expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
+
+ const inStockCheckbox = getInStockCheckbox();
+
+ if ( inStockCheckbox ) {
+ userEvent.click( inStockCheckbox );
+ }
+
+ await waitFor( () => {
+ expect( getInStockCheckbox()?.checked ).toBeTruthy();
+ expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
+ expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
+ } );
+ } );
+ } );
+
+ test( 'removes the option when it is clicked again', async () => {
+ await waitFor( async () => {
+ const ratingParam = 'instock,outofstock';
+ const {
+ getInStockCheckbox,
+ getOutOfStockCheckbox,
+ getOnBackorderCheckbox,
+ } = setupMultipleChoiceList( ratingParam );
+
+ expect( getInStockCheckbox()?.checked ).toBeTruthy();
+ expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
+
+ const inStockCheckbox = getInStockCheckbox();
+
+ if ( inStockCheckbox ) {
+ userEvent.click( inStockCheckbox );
+ }
+
+ await waitFor( () => {
+ expect( getInStockCheckbox()?.checked ).toBeFalsy();
+ expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
+ expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
+ } );
} );
} );
} );
diff --git a/plugins/woocommerce-blocks/assets/js/data/checkout/action-types.ts b/plugins/woocommerce-blocks/assets/js/data/checkout/action-types.ts
index f891e00a4bc..327cd10bca1 100644
--- a/plugins/woocommerce-blocks/assets/js/data/checkout/action-types.ts
+++ b/plugins/woocommerce-blocks/assets/js/data/checkout/action-types.ts
@@ -1,19 +1,20 @@
export const ACTION_TYPES = {
- SET_IDLE: 'SET_IDLE',
- SET_REDIRECT_URL: 'SET_REDIRECT_URL',
- SET_COMPLETE: 'SET_CHECKOUT_COMPLETE',
- SET_BEFORE_PROCESSING: 'SET_BEFORE_PROCESSING',
- SET_AFTER_PROCESSING: 'SET_AFTER_PROCESSING',
- SET_PROCESSING: 'SET_CHECKOUT_IS_PROCESSING',
- SET_HAS_ERROR: 'SET_CHECKOUT_HAS_ERROR',
- SET_CUSTOMER_ID: 'SET_CHECKOUT_CUSTOMER_ID',
- SET_ORDER_NOTES: 'SET_CHECKOUT_ORDER_NOTES',
- INCREMENT_CALCULATING: 'INCREMENT_CALCULATING',
DECREMENT_CALCULATING: 'DECREMENT_CALCULATING',
- SET_USE_SHIPPING_AS_BILLING: 'SET_USE_SHIPPING_AS_BILLING',
- SET_SHOULD_CREATE_ACCOUNT: 'SET_SHOULD_CREATE_ACCOUNT',
- SET_PREFERS_COLLECTION: 'SET_PREFERS_COLLECTION',
- SET_EXTENSION_DATA: 'SET_EXTENSION_DATA',
- SET_IS_CART: 'SET_IS_CART',
+ INCREMENT_CALCULATING: 'INCREMENT_CALCULATING',
SET_ADDITIONAL_FIELDS: 'SET_ADDITIONAL_FIELDS',
+ SET_AFTER_PROCESSING: 'SET_AFTER_PROCESSING',
+ SET_BEFORE_PROCESSING: 'SET_BEFORE_PROCESSING',
+ SET_COMPLETE: 'SET_CHECKOUT_COMPLETE',
+ SET_CUSTOMER_ID: 'SET_CHECKOUT_CUSTOMER_ID',
+ SET_CUSTOMER_PASSWORD: 'SET_CHECKOUT_CUSTOMER_PASSWORD',
+ SET_EXTENSION_DATA: 'SET_EXTENSION_DATA',
+ SET_HAS_ERROR: 'SET_CHECKOUT_HAS_ERROR',
+ SET_IDLE: 'SET_IDLE',
+ SET_IS_CART: 'SET_IS_CART',
+ SET_ORDER_NOTES: 'SET_CHECKOUT_ORDER_NOTES',
+ SET_PREFERS_COLLECTION: 'SET_PREFERS_COLLECTION',
+ SET_PROCESSING: 'SET_CHECKOUT_IS_PROCESSING',
+ SET_REDIRECT_URL: 'SET_REDIRECT_URL',
+ SET_SHOULD_CREATE_ACCOUNT: 'SET_SHOULD_CREATE_ACCOUNT',
+ SET_USE_SHIPPING_AS_BILLING: 'SET_USE_SHIPPING_AS_BILLING',
} as const;
diff --git a/plugins/woocommerce-blocks/assets/js/data/checkout/actions.ts b/plugins/woocommerce-blocks/assets/js/data/checkout/actions.ts
index 9065dc83b69..0f039081dfa 100644
--- a/plugins/woocommerce-blocks/assets/js/data/checkout/actions.ts
+++ b/plugins/woocommerce-blocks/assets/js/data/checkout/actions.ts
@@ -96,6 +96,16 @@ export const __internalSetCustomerId = ( customerId: number ) => ( {
customerId,
} );
+/**
+ * Set the customer password
+ *
+ * @param customerPassword Account password for the customer when creating accounts
+ */
+export const __internalSetCustomerPassword = ( customerPassword: string ) => ( {
+ type: types.SET_CUSTOMER_PASSWORD,
+ customerPassword,
+} );
+
/**
* Whether to use the shipping address as the billing address
*
@@ -170,6 +180,7 @@ export type CheckoutAction =
| typeof __internalIncrementCalculating
| typeof __internalDecrementCalculating
| typeof __internalSetCustomerId
+ | typeof __internalSetCustomerPassword
| typeof __internalSetUseShippingAsBilling
| typeof __internalSetShouldCreateAccount
| typeof __internalSetOrderNotes
diff --git a/plugins/woocommerce-blocks/assets/js/data/checkout/default-state.ts b/plugins/woocommerce-blocks/assets/js/data/checkout/default-state.ts
index 2afb25ad6d4..7891c255565 100644
--- a/plugins/woocommerce-blocks/assets/js/data/checkout/default-state.ts
+++ b/plugins/woocommerce-blocks/assets/js/data/checkout/default-state.ts
@@ -10,46 +10,36 @@ import { AdditionalValues } from '@woocommerce/settings';
import { STATUS, checkoutData } from './constants';
export type CheckoutState = {
- // Status of the checkout
- status: STATUS;
- // If any of the totals, taxes, shipping, etc need to be calculated, the count will be increased here
- calculatingCount: number;
- // True when the checkout is in an error state. Whatever caused the error (validation/payment method) will likely have triggered a notice.
- hasError: boolean;
- // This is the url that checkout will redirect to when it's ready.
- redirectUrl: string;
- // This is the ID for the draft order if one exists.
- orderId: number;
- // Order notes introduced by the user in the checkout form.
- orderNotes: string;
- // This is the ID of the customer the draft order belongs to.
- customerId: number;
- // Should the billing form be hidden and inherit the shipping address?
- useShippingAsBilling: boolean;
- // Should a user account be created?
- shouldCreateAccount: boolean;
- // If customer wants to checkout with a local pickup option.
- prefersCollection?: boolean | undefined;
- // Custom checkout data passed to the store API on processing.
- extensionData: Record< string, Record< string, unknown > >;
- // Additional fields values that are collected on Checkout.
- additionalFields: AdditionalValues;
+ additionalFields: AdditionalValues; // Additional fields values that are collected on Checkout.
+ calculatingCount: number; // If any of the totals, taxes, shipping, etc need to be calculated, the count will be increased here
+ customerId: number; // This is the ID of the customer the draft order belongs to.
+ customerPassword: string; // Customer password for account creation, if applicable.
+ extensionData: Record< string, Record< string, unknown > >; // Custom checkout data passed to the store API on processing.
+ hasError: boolean; // True when the checkout is in an error state. Whatever caused the error (validation/payment method) will likely have triggered a notice.
+ orderId: number; // This is the ID for the draft order if one exists.
+ orderNotes: string; // Order notes introduced by the user in the checkout form.
+ prefersCollection?: boolean | undefined; // If customer wants to checkout with a local pickup option.
+ redirectUrl: string; // This is the url that checkout will redirect to when it's ready.
+ shouldCreateAccount: boolean; // Should a user account be created?
+ status: STATUS; // Status of the checkout
+ useShippingAsBilling: boolean; // Should the billing form be hidden and inherit the shipping address?
};
export const defaultState: CheckoutState = {
- redirectUrl: '',
- status: STATUS.IDLE,
+ additionalFields: checkoutData.additional_fields || {},
+ calculatingCount: 0,
+ customerId: checkoutData.customer_id,
+ customerPassword: '',
+ extensionData: {},
hasError: false,
orderId: checkoutData.order_id,
- customerId: checkoutData.customer_id,
- calculatingCount: 0,
orderNotes: '',
+ prefersCollection: undefined,
+ redirectUrl: '',
+ shouldCreateAccount: false,
+ status: STATUS.IDLE,
useShippingAsBilling: isSameAddress(
checkoutData.billing_address,
checkoutData.shipping_address
),
- shouldCreateAccount: false,
- prefersCollection: undefined,
- extensionData: {},
- additionalFields: checkoutData.additional_fields || {},
};
diff --git a/plugins/woocommerce-blocks/assets/js/data/checkout/reducers.ts b/plugins/woocommerce-blocks/assets/js/data/checkout/reducers.ts
index 01425bbffc5..1a01c5772df 100644
--- a/plugins/woocommerce-blocks/assets/js/data/checkout/reducers.ts
+++ b/plugins/woocommerce-blocks/assets/js/data/checkout/reducers.ts
@@ -98,6 +98,15 @@ const reducer = ( state = defaultState, action: CheckoutAction ) => {
}
break;
+ case types.SET_CUSTOMER_PASSWORD:
+ if ( typeof action.customerPassword !== 'undefined' ) {
+ newState = {
+ ...state,
+ customerPassword: action.customerPassword,
+ };
+ }
+ break;
+
case types.SET_ADDITIONAL_FIELDS:
if ( action.additionalFields !== undefined ) {
newState = {
diff --git a/plugins/woocommerce-blocks/assets/js/data/checkout/selectors.ts b/plugins/woocommerce-blocks/assets/js/data/checkout/selectors.ts
index d30c22fe618..c472bc25bc8 100644
--- a/plugins/woocommerce-blocks/assets/js/data/checkout/selectors.ts
+++ b/plugins/woocommerce-blocks/assets/js/data/checkout/selectors.ts
@@ -16,6 +16,10 @@ export const getCustomerId = ( state: CheckoutState ) => {
return state.customerId;
};
+export const getCustomerPassword = ( state: CheckoutState ) => {
+ return state.customerPassword;
+};
+
export const getOrderId = ( state: CheckoutState ) => {
return state.orderId;
};
diff --git a/plugins/woocommerce-blocks/assets/js/icons/index.js b/plugins/woocommerce-blocks/assets/js/icons/index.js
index 270efde5c99..446bbadcebb 100644
--- a/plugins/woocommerce-blocks/assets/js/icons/index.js
+++ b/plugins/woocommerce-blocks/assets/js/icons/index.js
@@ -5,6 +5,7 @@ export { default as barcode } from './library/barcode';
export { default as cart } from './library/cart';
export { default as cartOutline } from './library/cart-outline';
export { default as checkPayment } from './library/check-payment';
+export { default as closeSquareShadow } from './library/close-square-shadow';
export { default as customerAccount } from './library/customer-account';
export { default as customerAccountStyle } from './library/customer-account-style';
export { default as customerAccountStyleAlt } from './library/customer-account-style-alt';
@@ -12,18 +13,26 @@ export { default as customerAccountStyleLine } from './library/customer-account-
export { default as eye } from './library/eye';
export { default as fields } from './library/fields';
export { default as filledCart } from './library/filled-cart';
+export { default as filter } from './library/filter';
+export { default as filterThreeLines } from './library/filter-three-lines';
export { default as folderStarred } from './library/folder-starred';
export { default as miniCart } from './library/mini-cart';
export { default as miniCartAlt } from './library/mini-cart-alt';
export { default as productDetails } from './library/product-details';
+export { default as productFilterActive } from './library/product-filter-active';
+export { default as productFilterAttribute } from './library/product-filter-attribute';
+export { default as productFilterOptions } from './library/product-filter-options';
+export { default as productFilterPrice } from './library/product-filter-price';
+export { default as productFilterRating } from './library/product-filter-rating';
+export { default as productFilterStockStatus } from './library/product-filter-stock-status';
export { default as productMeta } from './library/product-meta';
export { default as removeCart } from './library/remove-cart';
export { default as sparkles } from './library/sparkles';
export { default as stacks } from './library/stacks';
+export { default as thumbUp } from './library/thumb-up';
export { default as thumbnailsPositionBottom } from './library/thumbnails-position-bottom';
export { default as thumbnailsPositionLeft } from './library/thumbnails-position-left';
export { default as thumbnailsPositionRight } from './library/thumbnails-position-right';
-export { default as thumbUp } from './library/thumb-up';
export { default as toggle } from './library/toggle';
export { default as totals } from './library/totals';
export { default as woo } from './library/woo';
diff --git a/plugins/woocommerce-blocks/assets/js/icons/library/close-square-shadow.tsx b/plugins/woocommerce-blocks/assets/js/icons/library/close-square-shadow.tsx
new file mode 100644
index 00000000000..ac6ad85afcf
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/icons/library/close-square-shadow.tsx
@@ -0,0 +1,45 @@
+/**
+ * External dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const closeSquareShadow = (
+
+
+
+
+
+
+);
+
+export default closeSquareShadow;
diff --git a/plugins/woocommerce-blocks/assets/js/icons/library/customer-account-style-line.tsx b/plugins/woocommerce-blocks/assets/js/icons/library/customer-account-style-line.tsx
index 0439259e2a2..b8cd551c0c3 100644
--- a/plugins/woocommerce-blocks/assets/js/icons/library/customer-account-style-line.tsx
+++ b/plugins/woocommerce-blocks/assets/js/icons/library/customer-account-style-line.tsx
@@ -1,14 +1,21 @@
/**
* External dependencies
*/
-import { SVG } from '@wordpress/primitives';
+import { Circle, SVG, Path } from '@wordpress/primitives';
const customerAccountStyleLine = (
-
-
+
+
diff --git a/plugins/woocommerce-blocks/assets/js/icons/library/filter-three-lines.tsx b/plugins/woocommerce-blocks/assets/js/icons/library/filter-three-lines.tsx
new file mode 100644
index 00000000000..5ca1fd8101e
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/icons/library/filter-three-lines.tsx
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const filterThreeLines = (
+
+
+
+);
+
+export default filterThreeLines;
diff --git a/plugins/woocommerce-blocks/assets/js/icons/library/filter.tsx b/plugins/woocommerce-blocks/assets/js/icons/library/filter.tsx
new file mode 100644
index 00000000000..cd6204e4eb2
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/icons/library/filter.tsx
@@ -0,0 +1,12 @@
+/**
+ * External dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const filter = (
+
+
+
+);
+
+export default filter;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/active-filters/icon.tsx b/plugins/woocommerce-blocks/assets/js/icons/library/product-filter-active.tsx
similarity index 65%
rename from plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/active-filters/icon.tsx
rename to plugins/woocommerce-blocks/assets/js/icons/library/product-filter-active.tsx
index 2002cb4d543..fd7ffd102f0 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/active-filters/icon.tsx
+++ b/plugins/woocommerce-blocks/assets/js/icons/library/product-filter-active.tsx
@@ -1,18 +1,25 @@
-export const activeFiltersIcon = () => (
- (
+
-
-
-
+
);
+
+export default productFilterActive;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/icon.tsx b/plugins/woocommerce-blocks/assets/js/icons/library/product-filter-attribute.tsx
similarity index 86%
rename from plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/icon.tsx
rename to plugins/woocommerce-blocks/assets/js/icons/library/product-filter-attribute.tsx
index 879bbeb0a62..10dbe68c2c1 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/attribute-filter/icon.tsx
+++ b/plugins/woocommerce-blocks/assets/js/icons/library/product-filter-attribute.tsx
@@ -1,16 +1,23 @@
-export const attributeFilterIcon = () => (
- (
+
-
-
+
);
+
+export default productFilterAttribute;
diff --git a/plugins/woocommerce-blocks/assets/js/icons/library/product-filter-options.tsx b/plugins/woocommerce-blocks/assets/js/icons/library/product-filter-options.tsx
new file mode 100644
index 00000000000..ab0898455cd
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/icons/library/product-filter-options.tsx
@@ -0,0 +1,23 @@
+/**
+ * External dependencies
+ */
+import { SVG, Path } from '@wordpress/primitives';
+
+const productFilterOptions = (
+
+
+
+);
+
+export default productFilterOptions;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/price-filter/icon.tsx b/plugins/woocommerce-blocks/assets/js/icons/library/product-filter-price.tsx
similarity index 91%
rename from plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/price-filter/icon.tsx
rename to plugins/woocommerce-blocks/assets/js/icons/library/product-filter-price.tsx
index c5c1622387c..f6fc1720b7f 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/price-filter/icon.tsx
+++ b/plugins/woocommerce-blocks/assets/js/icons/library/product-filter-price.tsx
@@ -1,5 +1,10 @@
-export const priceFilterIcon = () => (
- (
+ (
stroke="currentColor"
strokeWidth="1.5"
/>
- (
strokeWidth="1.4"
fill="none"
/>
-
-
+
);
+
+export default productFilterPrice;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/rating-filter/icon.tsx b/plugins/woocommerce-blocks/assets/js/icons/library/product-filter-rating.tsx
similarity index 85%
rename from plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/rating-filter/icon.tsx
rename to plugins/woocommerce-blocks/assets/js/icons/library/product-filter-rating.tsx
index a00f4dcdbef..b4f6043b0ee 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/rating-filter/icon.tsx
+++ b/plugins/woocommerce-blocks/assets/js/icons/library/product-filter-rating.tsx
@@ -1,5 +1,10 @@
-export const ratingFilterIcon = () => (
- (
+ (
stroke="currentColor"
strokeWidth="1.5"
/>
-
-
+
);
+
+export default productFilterRating;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/stock-filter/icon.tsx b/plugins/woocommerce-blocks/assets/js/icons/library/product-filter-stock-status.tsx
similarity index 79%
rename from plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/stock-filter/icon.tsx
rename to plugins/woocommerce-blocks/assets/js/icons/library/product-filter-stock-status.tsx
index 95ef365bc50..3662662a0fb 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-filter/inner-blocks/stock-filter/icon.tsx
+++ b/plugins/woocommerce-blocks/assets/js/icons/library/product-filter-stock-status.tsx
@@ -1,5 +1,10 @@
-export const stockStatusFilterIcon = () => (
- (
+ (
stroke="currentColor"
strokeWidth="1.5"
/>
-
-
+
);
+
+export default productFilterStockStatus;
diff --git a/plugins/woocommerce-blocks/assets/js/previews/cart.ts b/plugins/woocommerce-blocks/assets/js/previews/cart.ts
index cf69d33d61a..52a8aa777bd 100644
--- a/plugins/woocommerce-blocks/assets/js/previews/cart.ts
+++ b/plugins/woocommerce-blocks/assets/js/previews/cart.ts
@@ -10,6 +10,7 @@ import { getSetting } from '@woocommerce/settings';
* Internal dependencies
*/
import { previewShippingRates } from './shipping-rates';
+import { API_SITE_CURRENCY, displayForMinorUnit } from './utils';
/**
* Prices from the API may change because of this display setting. This makes the response use either
@@ -81,16 +82,16 @@ export const previewCart: CartResponse = {
},
],
prices: {
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
- price: displayWithTax ? '12000' : '10000',
- regular_price: displayWithTax ? '12000' : '10000',
- sale_price: displayWithTax ? '12000' : '10000',
+ ...API_SITE_CURRENCY,
+ price: displayForMinorUnit(
+ displayWithTax ? '12000' : '10000'
+ ),
+ regular_price: displayForMinorUnit(
+ displayWithTax ? '120' : '100'
+ ),
+ sale_price: displayForMinorUnit(
+ displayWithTax ? '12000' : '10000'
+ ),
price_range: null,
raw_prices: {
precision: 6,
@@ -100,17 +101,11 @@ export const previewCart: CartResponse = {
},
},
totals: {
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
- line_subtotal: '2000',
- line_subtotal_tax: '400',
- line_total: '2000',
- line_total_tax: '400',
+ ...API_SITE_CURRENCY,
+ line_subtotal: displayForMinorUnit( '2000' ),
+ line_subtotal_tax: displayForMinorUnit( '400' ),
+ line_total: displayForMinorUnit( '2000' ),
+ line_total_tax: displayForMinorUnit( '400' ),
},
extensions: {},
item_data: [],
@@ -156,16 +151,14 @@ export const previewCart: CartResponse = {
},
],
prices: {
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
- price: displayWithTax ? '2400' : '2000',
- regular_price: displayWithTax ? '2400' : '2000',
- sale_price: displayWithTax ? '2400' : '2000',
+ ...API_SITE_CURRENCY,
+ price: displayForMinorUnit( displayWithTax ? '2400' : '2000' ),
+ regular_price: displayForMinorUnit(
+ displayWithTax ? '2400' : '2000'
+ ),
+ sale_price: displayForMinorUnit(
+ displayWithTax ? '2400' : '2000'
+ ),
price_range: null,
raw_prices: {
precision: 6,
@@ -175,17 +168,11 @@ export const previewCart: CartResponse = {
},
},
totals: {
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
- line_subtotal: '2000',
- line_subtotal_tax: '400',
- line_total: '2000',
- line_total_tax: '400',
+ ...API_SITE_CURRENCY,
+ line_subtotal: displayForMinorUnit( '2000' ),
+ line_subtotal_tax: displayForMinorUnit( '400' ),
+ line_total: displayForMinorUnit( '2000' ),
+ line_total_tax: displayForMinorUnit( '400' ),
},
extensions: {},
item_data: [],
@@ -195,6 +182,7 @@ export const previewCart: CartResponse = {
{
id: 1,
name: __( 'Polo', 'woocommerce' ),
+ slug: 'polo',
parent: 0,
type: 'simple',
variation: '',
@@ -204,16 +192,16 @@ export const previewCart: CartResponse = {
description: __( 'Polo', 'woocommerce' ),
on_sale: false,
prices: {
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
- price: displayWithTax ? '24000' : '20000',
- regular_price: displayWithTax ? '24000' : '20000',
- sale_price: displayWithTax ? '12000' : '10000',
+ ...API_SITE_CURRENCY,
+ price: displayForMinorUnit(
+ displayWithTax ? '24000' : '20000'
+ ),
+ regular_price: displayForMinorUnit(
+ displayWithTax ? '24000' : '20000'
+ ),
+ sale_price: displayForMinorUnit(
+ displayWithTax ? '12000' : '10000'
+ ),
price_range: null,
},
price_html: '',
@@ -252,6 +240,7 @@ export const previewCart: CartResponse = {
{
id: 2,
name: __( 'Long Sleeve Tee', 'woocommerce' ),
+ slug: 'long-sleeve-tee',
parent: 0,
type: 'simple',
variation: '',
@@ -261,16 +250,16 @@ export const previewCart: CartResponse = {
description: __( 'Long Sleeve Tee', 'woocommerce' ),
on_sale: false,
prices: {
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
- price: displayWithTax ? '30000' : '25000',
- regular_price: displayWithTax ? '30000' : '25000',
- sale_price: displayWithTax ? '30000' : '25000',
+ ...API_SITE_CURRENCY,
+ price: displayForMinorUnit(
+ displayWithTax ? '30000' : '25000'
+ ),
+ regular_price: displayForMinorUnit(
+ displayWithTax ? '30000' : '25000'
+ ),
+ sale_price: displayForMinorUnit(
+ displayWithTax ? '30000' : '25000'
+ ),
price_range: null,
},
price_html: '',
@@ -310,6 +299,7 @@ export const previewCart: CartResponse = {
{
id: 3,
name: __( 'Hoodie with Zipper', 'woocommerce' ),
+ slug: 'hoodie-with-zipper',
parent: 0,
type: 'simple',
variation: '',
@@ -319,16 +309,16 @@ export const previewCart: CartResponse = {
description: __( 'Hoodie with Zipper', 'woocommerce' ),
on_sale: true,
prices: {
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
- price: displayWithTax ? '15000' : '12500',
- regular_price: displayWithTax ? '30000' : '25000',
- sale_price: displayWithTax ? '15000' : '12500',
+ ...API_SITE_CURRENCY,
+ price: displayForMinorUnit(
+ displayWithTax ? '15000' : '12500'
+ ),
+ regular_price: displayForMinorUnit(
+ displayWithTax ? '30000' : '25000'
+ ),
+ sale_price: displayForMinorUnit(
+ displayWithTax ? '15000' : '12500'
+ ),
price_range: null,
},
price_html: '',
@@ -369,6 +359,7 @@ export const previewCart: CartResponse = {
{
id: 4,
name: __( 'Hoodie with Logo', 'woocommerce' ),
+ slug: 'hoodie-with-logo',
parent: 0,
type: 'simple',
variation: '',
@@ -378,16 +369,14 @@ export const previewCart: CartResponse = {
description: __( 'Polo', 'woocommerce' ),
on_sale: false,
prices: {
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
- price: displayWithTax ? '4500' : '4250',
- regular_price: displayWithTax ? '4500' : '4250',
- sale_price: displayWithTax ? '4500' : '4250',
+ ...API_SITE_CURRENCY,
+ price: displayForMinorUnit( displayWithTax ? '4500' : '4250' ),
+ regular_price: displayForMinorUnit(
+ displayWithTax ? '4500' : '4250'
+ ),
+ sale_price: displayForMinorUnit(
+ displayWithTax ? '4500' : '4250'
+ ),
price_range: null,
},
price_html: '',
@@ -427,6 +416,7 @@ export const previewCart: CartResponse = {
{
id: 5,
name: __( 'Hoodie with Pocket', 'woocommerce' ),
+ slug: 'hoodie-with-pocket',
parent: 0,
type: 'simple',
variation: '',
@@ -436,16 +426,14 @@ export const previewCart: CartResponse = {
description: __( 'Hoodie with Pocket', 'woocommerce' ),
on_sale: true,
prices: {
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
- price: displayWithTax ? '3500' : '3250',
- regular_price: displayWithTax ? '4500' : '4250',
- sale_price: displayWithTax ? '3500' : '3250',
+ ...API_SITE_CURRENCY,
+ price: displayForMinorUnit( displayWithTax ? '3500' : '3250' ),
+ regular_price: displayForMinorUnit(
+ displayWithTax ? '4500' : '4250'
+ ),
+ sale_price: displayForMinorUnit(
+ displayWithTax ? '3500' : '3250'
+ ),
price_range: null,
},
price_html: '',
@@ -486,6 +474,7 @@ export const previewCart: CartResponse = {
{
id: 6,
name: __( 'T-Shirt', 'woocommerce' ),
+ slug: 't-shirt',
parent: 0,
type: 'simple',
variation: '',
@@ -495,16 +484,14 @@ export const previewCart: CartResponse = {
description: __( 'T-Shirt', 'woocommerce' ),
on_sale: false,
prices: {
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
- price: displayWithTax ? '1800' : '1500',
- regular_price: displayWithTax ? '1800' : '1500',
- sale_price: displayWithTax ? '1800' : '1500',
+ ...API_SITE_CURRENCY,
+ price: displayForMinorUnit( displayWithTax ? '1800' : '1500' ),
+ regular_price: displayForMinorUnit(
+ displayWithTax ? '1800' : '1500'
+ ),
+ sale_price: displayForMinorUnit(
+ displayWithTax ? '1800' : '1500'
+ ),
price_range: null,
},
price_html: '',
@@ -546,15 +533,9 @@ export const previewCart: CartResponse = {
id: 'fee',
name: __( 'Fee', 'woocommerce' ),
totals: {
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
- total: '100',
- total_tax: '20',
+ ...API_SITE_CURRENCY,
+ total: displayForMinorUnit( '100' ),
+ total_tax: displayForMinorUnit( '20' ),
},
},
],
@@ -589,28 +570,22 @@ export const previewCart: CartResponse = {
phone: '',
},
totals: {
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
- total_items: '4000',
- total_items_tax: '800',
- total_fees: '100',
- total_fees_tax: '20',
+ ...API_SITE_CURRENCY,
+ total_items: displayForMinorUnit( '4000' ),
+ total_items_tax: displayForMinorUnit( '800' ),
+ total_fees: displayForMinorUnit( '100' ),
+ total_fees_tax: displayForMinorUnit( '20' ),
total_discount: '0',
total_discount_tax: '0',
total_shipping: '0',
total_shipping_tax: '0',
- total_tax: '820',
- total_price: '4920',
+ total_tax: displayForMinorUnit( '820' ),
+ total_price: displayForMinorUnit( '4920' ),
tax_lines: [
{
name: __( 'Sales tax', 'woocommerce' ),
rate: '20%',
- price: '820',
+ price: displayForMinorUnit( '820' ),
},
],
},
diff --git a/plugins/woocommerce-blocks/assets/js/previews/shipping-rates.ts b/plugins/woocommerce-blocks/assets/js/previews/shipping-rates.ts
index 8bd62b290f8..73ece153c1a 100644
--- a/plugins/woocommerce-blocks/assets/js/previews/shipping-rates.ts
+++ b/plugins/woocommerce-blocks/assets/js/previews/shipping-rates.ts
@@ -4,6 +4,11 @@
import { __, _x } from '@wordpress/i18n';
import type { CartResponseShippingRate } from '@woocommerce/types';
+/**
+ * Internal dependencies
+ */
+import { API_SITE_CURRENCY, displayForMinorUnit } from './utils';
+
export const previewShippingRates: CartResponseShippingRate[] = [
{
destination: {
@@ -38,32 +43,20 @@ export const previewShippingRates: CartResponseShippingRate[] = [
],
shipping_rates: [
{
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
+ ...API_SITE_CURRENCY,
name: __( 'Flat rate shipping', 'woocommerce' ),
description: '',
delivery_time: '',
- price: '500',
+ price: displayForMinorUnit( '500' ),
taxes: '0',
rate_id: 'flat_rate:0',
instance_id: 0,
meta_data: [],
method_id: 'flat_rate',
- selected: true,
+ selected: false,
},
{
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
+ ...API_SITE_CURRENCY,
name: __( 'Free shipping', 'woocommerce' ),
description: '',
delivery_time: '',
@@ -73,16 +66,10 @@ export const previewShippingRates: CartResponseShippingRate[] = [
instance_id: 0,
meta_data: [],
method_id: 'flat_rate',
- selected: false,
+ selected: true,
},
{
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
+ ...API_SITE_CURRENCY,
name: __( 'Local pickup', 'woocommerce' ),
description: '',
delivery_time: '',
@@ -104,13 +91,7 @@ export const previewShippingRates: CartResponseShippingRate[] = [
selected: false,
},
{
- currency_code: 'USD',
- currency_symbol: '$',
- currency_minor_unit: 2,
- currency_decimal_separator: '.',
- currency_thousand_separator: ',',
- currency_prefix: '$',
- currency_suffix: '',
+ ...API_SITE_CURRENCY,
name: __( 'Local pickup', 'woocommerce' ),
description: '',
delivery_time: '',
diff --git a/plugins/woocommerce-blocks/assets/js/previews/utils.ts b/plugins/woocommerce-blocks/assets/js/previews/utils.ts
new file mode 100644
index 00000000000..d83d6ee50fa
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/previews/utils.ts
@@ -0,0 +1,34 @@
+/**
+ * External dependencies
+ */
+import { SITE_CURRENCY } from '@woocommerce/settings';
+
+/**
+ * Put site currency back in API format for the responses.
+ */
+export const API_SITE_CURRENCY = {
+ currency_code: SITE_CURRENCY.code,
+ currency_symbol: SITE_CURRENCY.symbol,
+ currency_minor_unit: SITE_CURRENCY.minorUnit,
+ currency_decimal_separator: SITE_CURRENCY.decimalSeparator,
+ currency_thousand_separator: SITE_CURRENCY.thousandSeparator,
+ currency_prefix: SITE_CURRENCY.prefix,
+ currency_suffix: SITE_CURRENCY.suffix,
+};
+
+/**
+ * Preview data is defined with 2dp. This converts to selected currency settings.
+ */
+export const displayForMinorUnit = ( value: string ): string => {
+ const minorUnit = SITE_CURRENCY.minorUnit;
+
+ // Preview data is defined with 2 dp.
+ if ( minorUnit === 2 ) {
+ return value;
+ }
+
+ const multiplier = Math.pow( 10, minorUnit );
+ const intValue = Math.round( parseInt( value, 10 ) / Math.pow( 10, 2 ) );
+
+ return ( intValue * multiplier ).toString();
+};
diff --git a/plugins/woocommerce-blocks/assets/js/settings/shared/default-constants.ts b/plugins/woocommerce-blocks/assets/js/settings/shared/default-constants.ts
index eaa737a8baa..90324849712 100644
--- a/plugins/woocommerce-blocks/assets/js/settings/shared/default-constants.ts
+++ b/plugins/woocommerce-blocks/assets/js/settings/shared/default-constants.ts
@@ -1,14 +1,19 @@
+/**
+ * External dependencies
+ */
+import type { Currency, SymbolPosition } from '@woocommerce/types';
+
/**
* Internal dependencies
*/
import { allSettings } from './settings-init';
+import { getCurrencyPrefix, getCurrencySuffix } from './utils';
/**
* This exports all default core settings as constants.
*/
export const ADMIN_URL = allSettings.adminUrl;
export const COUNTRIES = allSettings.countries;
-export const CURRENCY = allSettings.currency;
export const CURRENT_USER_IS_ADMIN = allSettings.currentUserIsAdmin as boolean;
export const HOME_URL = allSettings.homeUrl as string | undefined;
export const LOCALE = allSettings.locale;
@@ -27,3 +32,22 @@ export const WC_ASSET_URL = allSettings.wcAssetUrl;
export const WC_VERSION = allSettings.wcVersion;
export const WP_LOGIN_URL = allSettings.wpLoginUrl;
export const WP_VERSION = allSettings.wpVersion;
+
+// Settings from the server in WooCommerceSiteCurrency format.
+export const CURRENCY = allSettings.currency;
+// Convert WooCommerceSiteCurrency format to Currency format.
+export const SITE_CURRENCY: Currency = {
+ code: CURRENCY.code,
+ symbol: CURRENCY.symbol,
+ thousandSeparator: CURRENCY.thousandSeparator,
+ decimalSeparator: CURRENCY.decimalSeparator,
+ minorUnit: CURRENCY.precision,
+ prefix: getCurrencyPrefix(
+ CURRENCY.symbol,
+ CURRENCY.symbolPosition as SymbolPosition
+ ),
+ suffix: getCurrencySuffix(
+ CURRENCY.symbol,
+ CURRENCY.symbolPosition as SymbolPosition
+ ),
+};
diff --git a/plugins/woocommerce-blocks/assets/js/settings/shared/default-fields.ts b/plugins/woocommerce-blocks/assets/js/settings/shared/default-fields.ts
index 6545b7aea6f..829d12e849e 100644
--- a/plugins/woocommerce-blocks/assets/js/settings/shared/default-fields.ts
+++ b/plugins/woocommerce-blocks/assets/js/settings/shared/default-fields.ts
@@ -40,6 +40,8 @@ export interface FormField {
type?: string;
// The options if this is a select field
options?: ComboboxControlOption[];
+ // The placeholder for the field, only applicable for select fields.
+ placeholder?: string;
// Additional attributes added when registering a field. String in key is required for data attributes.
attributes?: Record< keyof CustomFieldAttributes, string >;
}
diff --git a/plugins/woocommerce-blocks/assets/js/settings/shared/utils.ts b/plugins/woocommerce-blocks/assets/js/settings/shared/utils.ts
index df2a01fafd3..fd5c5a395a6 100644
--- a/plugins/woocommerce-blocks/assets/js/settings/shared/utils.ts
+++ b/plugins/woocommerce-blocks/assets/js/settings/shared/utils.ts
@@ -2,6 +2,7 @@
* External dependencies
*/
import compareVersions from 'compare-versions';
+import type { SymbolPosition } from '@woocommerce/types';
/**
* Internal dependencies
@@ -138,3 +139,35 @@ export const getPaymentMethodData = (
>;
return paymentMethodData[ paymentMethodId ] ?? defaultValue;
};
+
+/**
+ * Get currency prefix.
+ */
+export const getCurrencyPrefix = (
+ symbol: string,
+ symbolPosition: SymbolPosition
+): string => {
+ const prefixes = {
+ left: symbol,
+ left_space: symbol + ' ',
+ right: '',
+ right_space: '',
+ };
+ return prefixes[ symbolPosition ] || '';
+};
+
+/**
+ * Get currency suffix.
+ */
+export const getCurrencySuffix = (
+ symbol: string,
+ symbolPosition: SymbolPosition
+): string => {
+ const suffixes = {
+ left: '',
+ left_space: '',
+ right: symbol,
+ right_space: ' ' + symbol,
+ };
+ return suffixes[ symbolPosition ] || '';
+};
diff --git a/plugins/woocommerce-blocks/bin/add-split-chunk-dependencies.js b/plugins/woocommerce-blocks/bin/add-split-chunk-dependencies.js
new file mode 100644
index 00000000000..245186faffb
--- /dev/null
+++ b/plugins/woocommerce-blocks/bin/add-split-chunk-dependencies.js
@@ -0,0 +1,75 @@
+// The Dependency Extraction Webpack Plugin does not add split chunks as dependencies
+// to the .asset.php files that list dependencies. In the past we manually enqueued
+// those dependencies in PHP.
+// For every generated .asset.php file in the whole bundle, this plugin prefixes the
+// list of dependencies with the handles of split chunks that were generated in the build.
+
+// e.g. if your bundle has a vendors script called foo-vendors then for every entry-point
+// that has a .asset.php file generated by Dependency Extraction Webpack Plugin
+// the plugin will edit that file to include foo-vendors as a listed dependency.
+
+// This means for any split chunk you build you'll only need to register it in PHP, but all
+// files that depend on it will automatically include it as a dependency.
+class AddSplitChunkDependencies {
+ apply( compiler ) {
+ compiler.hooks.thisCompilation.tap(
+ 'AddStableChunksToAssets',
+ ( compilation ) => {
+ compilation.hooks.processAssets.tap(
+ {
+ name: 'AddStableChunksToAssets',
+ stage: compiler.webpack.Compilation
+ .PROCESS_ASSETS_STAGE_ANALYSE,
+ },
+ () => {
+ const { chunks } = compilation;
+
+ const splitChunks = chunks.filter( ( chunk ) => {
+ return chunk?.chunkReason?.includes( 'split' );
+ } );
+
+ // find files that have an asset.php file
+ const chunksToAddSplitsTo = chunks.filter(
+ ( chunk ) => {
+ return (
+ ! chunk?.chunkReason?.includes( 'split' ) &&
+ chunk.files.find( ( file ) =>
+ file.endsWith( 'asset.php' )
+ )
+ );
+ }
+ );
+
+ for ( const chunk of chunksToAddSplitsTo ) {
+ const assetFile = chunk.files.find( ( file ) =>
+ file.endsWith( 'asset.php' )
+ );
+
+ const assetFileContent = compilation.assets[
+ assetFile
+ ]
+ .source()
+ .toString();
+
+ const extraDependencies = splitChunks
+ .map( ( c ) => `'${ c.name }'` )
+ .join( ', ' );
+
+ const updatedFileContent = assetFileContent.replace(
+ /('dependencies'\s*=>\s*array\s*\(\s*)([^)]*)\)/,
+ `$1${ extraDependencies }, $2)`
+ );
+
+ compilation.assets[ assetFile ] = {
+ source: () => updatedFileContent,
+ size: () => updatedFileContent.length,
+ };
+ }
+ }
+ );
+ }
+ );
+ }
+}
+
+module.exports = AddSplitChunkDependencies;
diff --git a/plugins/woocommerce-blocks/bin/change-versions.sh b/plugins/woocommerce-blocks/bin/change-versions.sh
deleted file mode 100755
index ef2424221c5..00000000000
--- a/plugins/woocommerce-blocks/bin/change-versions.sh
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/bin/sh
-# Functions
-# Check if string contains substring
-is_substring() {
- case "$2" in
- *$1*)
- return 0
- ;;
- *)
- return 1
- ;;
- esac
-}
-
-# Output colorized strings
-#
-# Color codes:
-# 0 - black
-# 1 - red
-# 2 - green
-# 3 - yellow
-# 4 - blue
-# 5 - magenta
-# 6 - cian
-# 7 - white
-output() {
- echo "$(tput setaf "$1")$2$(tput sgr0)"
-}
-
-if [ ! $VERSION ]; then
- output 3 "Please enter the version number, for example, 1.0.0:"
- read -r VERSION
-fi
-
-output 2 "Updating version numbers in files..."
-
-IS_PRE_RELEASE=false
-# Check if is a pre-release.
-if is_substring "-" "${VERSION}"; then
- IS_PRE_RELEASE=true
- output 4 "Detected pre-release version."
-fi
-
-if [ $IS_PRE_RELEASE = false ]; then
- # Replace all instances of $VID:$ with the release version but only if not pre-release.
- find ./src woocommerce-gutenberg-products-block.php -name "*.php" -print0 | xargs -0 perl -i -pe 's/\$VID:\$/'${VERSION}'/g'
- # Update version number in readme.txt but only if not pre-release.
- perl -i -pe 's/Stable tag:*.+/Stable tag: '${VERSION}'/' readme.txt
- output 2 "Version numbers updated in readme.txt and \$VID:\$ instances."
-else
- output 4 "Note: pre-releases will not have the readme.txt stable tag updated."
-fi
-
-# Update version in main plugin file.
-perl -i -pe 's/Version:*.+/Version: '${VERSION}'/' woocommerce-gutenberg-products-block.php
-
-# Update version in package.json.
-perl -i -pe 's/"version":*.+/"version": "'${VERSION}'",/' package.json
-
-# Update version in package-lock.json.
-perl -i -0777 -pe 's/"name": "\@woocommerce\/block-library",\s*\K"version":*.+\n/"version": "'${VERSION}'",\n/g' package-lock.json
-
-# Update version in src/Package.php.
-perl -i -pe "s/version \= '*.+';/version = '${VERSION}';/" src/Package.php
-
-# Update version in composer.json.
-perl -i -pe 's/"version":*.+/"version": "'${VERSION}'",/' composer.json
-
-output 2 "Version numbers updated in main plugin file, package.json, package-lock.json, src/Package.php and composer.json."
diff --git a/plugins/woocommerce-blocks/bin/changelog/README.md b/plugins/woocommerce-blocks/bin/changelog/README.md
deleted file mode 100644
index 5146467edf6..00000000000
--- a/plugins/woocommerce-blocks/bin/changelog/README.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# Changelog Script
-
-This folder contains the logic for a changelog script that can be used for generating changelog entries from either pull requests added to a GitHub milestone, or pull requests that are part of a Zenhub release.
-
-## Usage
-
-By default, changelog entries will use the title of pull requests. However, you can also customize the changelog entry by adding to the description of the pull custom text in the following format.
-
-```md
-### Changelog
-
-> Fix bug in Safari and other Webkit browsers.
-```
-
-You can implement the script in your `package.json` in the simplest form by adding the following to the `"scripts"` property (assuming it is installed in `./bin`):
-
-```json
-{
- "scripts": {
- "changelog": "node ./bin/changelog"
- }
-}
-```
-
-## Configuration
-
-The following configuration options can be set for the changelog script. **Note:** you can use all of these options but environment variables overwrite `package.json` config and command line arguments overwrite environment variables.
-
-`package.json` configuration should be added on a top level `changelog` property.
-
-The 'variable' in the following table can be used in `package.json` or as a cli arg.
-
-| variable | description |
-| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| labelPrefix | Any labels prefixed with this string will be used to derive the "type" of change (defaults to `type:`). |
-| skipLabel | Any pull having this label will be skipped for the changelog (defaults to `no-changelog`). |
-| defaultPrefix | When there is no label with the `labelPrefix` on a pull, this is the default type that will be used for the changelog entry (defaults to `dev`). |
-| changelogSrcType | Either "MILESTONE" (default) or "ZENHUB_RELEASE". This determines what will serve as the source for the changelog entries. |
-| devNoteLabel | If a pull has this label then `[DN]` will be appended to the end of the changelog. It's a good way to indicate what entries have (or will have) dev notes. |
-| repo | This is the namespace for the GitHub repository used as the source for pulls used in the changelog entries. Example: `'woocommerce/woocommerce-gutenberg-products-block'` |
-| githubToken | You can pass your GitHub API token to the script. NOTE: Strongly recommend you use environment variable for this (`GITHUB_TOKEN`). |
-| zhApiKey | You can pass your Zenhub api key to the script using this config. NOTE: Strongly recommend you use environment variable for this. |
-
-The two environment variables you can use are:
-
-| Environment Variable | Description |
-| -------------------- | ------------------------------------------------------------- |
-| GITHUB_TOKEN | GitHub API token for authorizing on the GitHub API. |
-| ZH_API_TOKEN | Zenhub API token used for authorizing against the Zenhub API. |
-
-### Examples
-
-#### package.json
-
-```json
-{
- "changelog": {
- "labelPrefix": "type:",
- "skipLabel": "skip-changelog",
- "defaultPrefix": "dev",
- "repo": "woocommerce/woocommerce-gutenberg-products-block"
- }
-}
-```
-
-#### Environment Variable
-
-```bash
-GITHUB_TOKEN="1343ASDFQWER13241REASD" node ./bin/changelog
-```
-
-#### Command Line
-
-```bash
-node ./bin/changelog --labelPrefix="type:" --skipLabel="skip-changelog" --defaultPrefix="dev" --repo="woocommerce/woocommerce-gutenberg-products-block" --githubToken="1343ASDFQWER13241REASD"
-```
diff --git a/plugins/woocommerce-blocks/bin/changelog/common/get-entry.js b/plugins/woocommerce-blocks/bin/changelog/common/get-entry.js
deleted file mode 100644
index cb8e5466176..00000000000
--- a/plugins/woocommerce-blocks/bin/changelog/common/get-entry.js
+++ /dev/null
@@ -1,88 +0,0 @@
-'use strict';
-
-const requestPromise = require( 'request-promise' );
-const { graphql } = require( '@octokit/graphql' );
-const { pkg, REPO } = require( '../config' );
-
-/* eslint no-console: 0 */
-
-const headers = {
- authorization: `token ${ pkg.changelog.githubToken }`,
- 'user-agent': 'changelog-tool',
-};
-
-const authedGraphql = graphql.defaults( { headers } );
-
-const getPullRequestType = ( labels ) => {
- const typeLabel = labels.find( ( label ) =>
- label.name.includes( pkg.changelog.labelPrefix )
- );
- if ( ! typeLabel ) {
- return pkg.changelog.defaultPrefix;
- }
- return typeLabel.name.replace( `${ pkg.changelog.labelPrefix } `, '' );
-};
-
-const devNoteSuffix = ( labels ) => {
- const noteLabel = labels.find( ( label ) =>
- label.name.includes( pkg.changelog.devNoteLabel )
- );
- return noteLabel ? ' [DN]' : '';
-};
-
-const isCollaborator = async ( username ) => {
- return requestPromise( {
- url: `https://api.github.com/orgs/${
- REPO.split( '/' )[ 0 ]
- }/members/${ username }`,
- headers,
- resolveWithFullResponse: true,
- } )
- .then( ( response ) => {
- return response.statusCode === 204;
- } )
- .catch( ( err ) => {
- if ( err.statusCode !== 404 ) {
- console.log( '🤯' );
- console.log( err.message );
- }
- } );
-};
-
-const getEntry = async ( pullRequest ) => {
- if (
- pullRequest.labels.nodes.some(
- ( label ) => label.name === pkg.changelog.skipLabel
- )
- ) {
- return;
- }
-
- const collaborator = await isCollaborator( pullRequest.author.login );
- const type = getPullRequestType( pullRequest.labels.nodes );
- const authorTag = collaborator ? '' : `👏 @${ pullRequest.author.login }`;
- const devNote = devNoteSuffix( pullRequest.labels.nodes );
- let title;
- if ( /### Changelog\r\n\r\n> /.test( pullRequest.body ) ) {
- const bodyParts = pullRequest.body.split( '### Changelog\r\n\r\n> ' );
- const note = bodyParts[ bodyParts.length - 1 ];
- title = note
- // Remove comment prompt
- .replace( //gm, '' )
- // Remove new lines and whitespace
- .trim();
- if ( ! title.length ) {
- title = `${ type }: ${ pullRequest.title }`;
- } else {
- title = `${ type }: ${ title }`;
- }
- } else {
- title = `${ type }: ${ pullRequest.title }`;
- }
- return `- ${ title } [#${ pullRequest.number }](${ pullRequest.url })${ devNote } ${ authorTag }`;
-};
-
-module.exports = {
- authedGraphql,
- getEntry,
-};
diff --git a/plugins/woocommerce-blocks/bin/changelog/common/index.js b/plugins/woocommerce-blocks/bin/changelog/common/index.js
deleted file mode 100644
index f4e9a949217..00000000000
--- a/plugins/woocommerce-blocks/bin/changelog/common/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-const { authedGraphql } = require( './get-entry' );
-const { make } = require( './make' );
-
-module.exports = { authedGraphql, make };
diff --git a/plugins/woocommerce-blocks/bin/changelog/common/make.js b/plugins/woocommerce-blocks/bin/changelog/common/make.js
deleted file mode 100644
index 957e495adf2..00000000000
--- a/plugins/woocommerce-blocks/bin/changelog/common/make.js
+++ /dev/null
@@ -1,34 +0,0 @@
-'use strict';
-
-const chalk = require( 'chalk' );
-const { getEntry } = require( './get-entry' );
-
-/* eslint no-console: 0*/
-
-const make = async ( pullRequestFetcher, version ) => {
- const pullRequests = await pullRequestFetcher( version );
- let entries = await Promise.all(
- pullRequests.map( async ( pr ) => await getEntry( pr ) )
- );
- if ( ! entries || ! entries.length ) {
- console.log(
- chalk.yellow( "This version doesn't have any associated PR." )
- );
- return;
- }
-
- entries = entries.filter( Boolean );
-
- if ( ! entries || ! entries.length ) {
- console.log(
- chalk.yellow(
- 'None of the PRs of this version are eligible for the changelog.'
- )
- );
- return;
- }
- entries.sort();
- console.log( entries.join( '\n' ) );
-};
-
-module.exports = { make };
diff --git a/plugins/woocommerce-blocks/bin/changelog/config.js b/plugins/woocommerce-blocks/bin/changelog/config.js
deleted file mode 100644
index 43571137b45..00000000000
--- a/plugins/woocommerce-blocks/bin/changelog/config.js
+++ /dev/null
@@ -1,46 +0,0 @@
-'use strict';
-
-const pkg = require( '../../package.json' );
-const Config = require( 'merge-config' );
-
-const config = new Config();
-
-const changelogSrcTypes = {
- MILESTONE: 'MILESTONE',
- ZENHUB: 'ZENHUB_RELEASE',
-};
-
-const DEFAULTS = {
- labelPrefix: 'type:',
- skipLabel: 'no-changelog',
- defaultPrefix: 'dev',
- changelogSrcType: changelogSrcTypes.MILESTONE,
- devNoteLabel: 'dev-note',
- repo: '',
- githubToken: '',
- zhApiToken: '',
-};
-
-pkg.changelog = pkg.changelog || DEFAULTS;
-
-config.merge( { ...DEFAULTS, ...pkg.changelog } );
-config.env( [ 'GITHUB_TOKEN', 'ZH_API_TOKEN' ] );
-config.argv( Object.keys( DEFAULTS ) );
-
-const REPO = config.get( 'repo' );
-
-if ( ! REPO ) {
- throw new Error(
- "The 'repo' configuration value is not set. This script requires the\n" +
- 'repository namespace used as the source for the changelog entries.'
- );
-}
-
-module.exports = {
- pkg: {
- ...pkg,
- changelog: config.get(),
- },
- REPO,
- changelogSrcTypes,
-};
diff --git a/plugins/woocommerce-blocks/bin/changelog/github/index.js b/plugins/woocommerce-blocks/bin/changelog/github/index.js
deleted file mode 100644
index c3035d7d3a2..00000000000
--- a/plugins/woocommerce-blocks/bin/changelog/github/index.js
+++ /dev/null
@@ -1,7 +0,0 @@
-'use-strict';
-
-const { makeChangeLog } = require( './make-change-log' );
-
-module.exports = {
- makeChangeLog,
-};
diff --git a/plugins/woocommerce-blocks/bin/changelog/github/make-change-log.js b/plugins/woocommerce-blocks/bin/changelog/github/make-change-log.js
deleted file mode 100644
index c40ed639da0..00000000000
--- a/plugins/woocommerce-blocks/bin/changelog/github/make-change-log.js
+++ /dev/null
@@ -1,70 +0,0 @@
-'use strict';
-const chalk = require( 'chalk' );
-const promptly = require( 'promptly' );
-const { REPO, pkg } = require( '../config' );
-const { make } = require( '../common' );
-const { fetchAllPullRequests } = require( './requests' );
-
-/* eslint no-console: 0 */
-let ready = false;
-
-const makeChangeLog = async () => {
- if ( ! pkg.changelog.githubToken ) {
- console.log(
- chalk.yellow(
- 'This program requires an api token. You can create one here: '
- ) + 'https://github.com/settings/tokens'
- );
- console.log( '' );
- console.log(
- chalk.yellow(
- 'Token scope will require read permissions on public_repo, admin:org, and user.'
- )
- );
- console.log( '' );
- console.log(
- chalk.yellow(
- 'Export the token as variable called GITHUB_TOKEN from your bash profile.'
- )
- );
- console.log( '' );
-
- ready = await promptly.confirm( 'Are you ready to continue? ' );
- } else {
- console.log( chalk.green( 'Detected GITHUB_TOKEN is set.' ) );
- ready = true;
- }
-
- if ( ready ) {
- console.log( '' );
- console.log(
- chalk.yellow(
- 'In order to generate the changelog, you will have to provide a version number to retrieve the PRs from.'
- )
- );
- console.log( '' );
- console.log(
- chalk.yellow( 'Write it as it appears in the milestones page: ' ) +
- `https://github.com/${ REPO }/milestones`
- );
- console.log( '' );
- const version = await promptly.prompt( 'Version number: ' );
- console.log( '' );
- console.log(
- chalk.green(
- 'Here is the generated changelog. Be sure to remove entries ' +
- `not intended for a ${ pkg.title } release.`
- )
- );
- console.log( '' );
- make( fetchAllPullRequests, version );
- } else {
- console.log( '' );
- console.log( chalk.yellow( 'Ok, see you soon.' ) );
- console.log( '' );
- }
-};
-
-module.exports = {
- makeChangeLog,
-};
diff --git a/plugins/woocommerce-blocks/bin/changelog/github/requests.js b/plugins/woocommerce-blocks/bin/changelog/github/requests.js
deleted file mode 100644
index aaf06680f4c..00000000000
--- a/plugins/woocommerce-blocks/bin/changelog/github/requests.js
+++ /dev/null
@@ -1,93 +0,0 @@
-'use strict';
-
-const { REPO } = require( '../config' );
-const { authedGraphql } = require( '../common' );
-
-/* eslint no-console: 0 */
-
-const getMilestoneNumber = async ( version ) => {
- const [ owner, repo ] = REPO.split( '/' );
- const query = `
- {
- repository(owner: "${ owner }", name: "${ repo }") {
- milestones(last: 50) {
- nodes {
- title
- number
- }
- }
- }
- }
- `;
- const data = await authedGraphql( query );
- const matchingNode = data.repository.milestones.nodes.find(
- ( node ) => node.title === version
- );
- if ( ! matchingNode ) {
- throw new Error(
- `Unable to find a milestone matching the given version ${ version }`
- );
- }
- return matchingNode.number;
-};
-
-const getQuery = ( milestoneNumber, before ) => {
- const [ owner, repo ] = REPO.split( '/' );
- const paging = before ? `, before: "${ before }"` : '';
- return `
- {
- repository(owner: "${ owner }", name: "${ repo }") {
- milestone(number: ${ milestoneNumber }) {
- pullRequests(last: 100, states: [MERGED]${ paging }) {
- totalCount
- pageInfo {
- hasPreviousPage
- startCursor
- }
- nodes {
- number
- title
- url
- author {
- login
- }
- body
- labels(last: 10) {
- nodes {
- name
- }
- }
- }
- }
- }
- }
- }
- `;
-};
-
-const fetchAllPullRequests = async ( version ) =>
- await ( async () => {
- const milestoneNumber = await getMilestoneNumber( version );
- const fetchResults = async ( before ) => {
- const query = getQuery( milestoneNumber, before );
- const results = await authedGraphql( query );
- if (
- results.repository.milestone.pullRequests.pageInfo
- .hasPreviousPage === false
- ) {
- return results.repository.milestone.pullRequests.nodes;
- }
-
- const nextResults = await fetchResults(
- results.repository.milestone.pullRequests.pageInfo.startCursor
- );
- return results.repository.milestone.pullRequests.nodes.concat(
- nextResults
- );
- };
- return await fetchResults();
- } )();
-
-module.exports = {
- fetchAllPullRequests,
-};
diff --git a/plugins/woocommerce-blocks/bin/changelog/index.js b/plugins/woocommerce-blocks/bin/changelog/index.js
deleted file mode 100644
index 975c1051ac6..00000000000
--- a/plugins/woocommerce-blocks/bin/changelog/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env node
-'use strict';
-/* eslint no-console: 0 */
-const chalk = require( 'chalk' );
-
-try {
- const { makeChangeLog: githubMake } = require( './github' );
- const { makeChangeLog: zenhubMake } = require( './zenhub' );
- const { pkg, changelogSrcTypes } = require( './config' );
-
- const makeChangeLog =
- pkg.changelog.changelogSrcType === changelogSrcTypes.ZENHUB
- ? zenhubMake
- : githubMake;
-
- makeChangeLog();
-} catch ( error ) {
- console.log( chalk.red( error.message ) );
-}
diff --git a/plugins/woocommerce-blocks/bin/changelog/zenhub/index.js b/plugins/woocommerce-blocks/bin/changelog/zenhub/index.js
deleted file mode 100644
index c3035d7d3a2..00000000000
--- a/plugins/woocommerce-blocks/bin/changelog/zenhub/index.js
+++ /dev/null
@@ -1,7 +0,0 @@
-'use-strict';
-
-const { makeChangeLog } = require( './make-change-log' );
-
-module.exports = {
- makeChangeLog,
-};
diff --git a/plugins/woocommerce-blocks/bin/changelog/zenhub/make-change-log.js b/plugins/woocommerce-blocks/bin/changelog/zenhub/make-change-log.js
deleted file mode 100644
index 484758454cc..00000000000
--- a/plugins/woocommerce-blocks/bin/changelog/zenhub/make-change-log.js
+++ /dev/null
@@ -1,88 +0,0 @@
-'use strict';
-const chalk = require( 'chalk' );
-const promptly = require( 'promptly' );
-const { pkg } = require( '../config' );
-const { make } = require( '../common' );
-const { fetchAllPullRequests } = require( './requests' );
-
-/* eslint no-console: 0 */
-let ready = false;
-
-const makeChangeLog = async () => {
- if ( ! pkg.changelog.zhApiToken || ! pkg.changelog.githubToken ) {
- const zenhubSet = pkg.changelog.zhApiToken
- ? chalk.green( 'set' )
- : chalk.red( 'not set' );
- const githubSet = pkg.changelog.githubToken
- ? chalk.green( 'set' )
- : chalk.red( 'not set' );
- console.log( `${ chalk.yellow( 'Zenhub Token:' ) } ${ zenhubSet }` );
- console.log( `${ chalk.yellow( 'GitHub Token:' ) } ${ githubSet }` );
- console.log( '' );
- console.log(
- chalk.yellow(
- 'This program requires an api token from GitHub and Zenhub.'
- )
- );
- console.log(
- chalk.yellow(
- 'You can create and get a GitHub token here: https://github.com/settings/tokens'
- )
- );
- console.log(
- chalk.yellow(
- 'You can create and get a Zenhub token here: https://app.zenhub.com/dashboard/tokens'
- )
- );
- console.log( '' );
- console.log(
- chalk.yellow(
- 'Token scope for GitHub will require read permissions on public_repo, admin:org, and user.'
- )
- );
- console.log( '' );
- console.log(
- chalk.yellow(
- 'Export the github token as variable called GITHUB_TOKEN and the Zenhub token as a variable called ZH_API_TOKEN from your bash profile.'
- )
- );
- console.log( '' );
-
- ready = await promptly.confirm( 'Are you ready to continue? ' );
- } else {
- console.log(
- chalk.green(
- 'Detected that ZH_API_TOKEN and GITHUB_TOKEN values are set.'
- )
- );
- ready = true;
- }
-
- if ( ready ) {
- console.log( '' );
- console.log(
- chalk.yellow(
- 'In order to generate the changelog, you will have to provide the Zenhub release ID to retrieve the PRs from. You can get that from `release` param value in the url of the release report page.'
- )
- );
- console.log( '' );
- const releaseId = await promptly.prompt( 'Release Id: ' );
- console.log( '' );
- console.log(
- chalk.green(
- 'Here is the generated changelog. Be sure to remove entries ' +
- `not intended for a ${ pkg.title } release. All entries with the ${ pkg.changelog.skipLabel } label have been skipped`
- )
- );
- console.log( '' );
- make( fetchAllPullRequests, releaseId );
- } else {
- console.log( '' );
- console.log( chalk.yellow( 'Ok, see you soon.' ) );
- console.log( '' );
- }
-};
-
-module.exports = {
- makeChangeLog,
-};
diff --git a/plugins/woocommerce-blocks/bin/changelog/zenhub/requests.js b/plugins/woocommerce-blocks/bin/changelog/zenhub/requests.js
deleted file mode 100644
index 22ddf81de26..00000000000
--- a/plugins/woocommerce-blocks/bin/changelog/zenhub/requests.js
+++ /dev/null
@@ -1,100 +0,0 @@
-'use strict';
-
-/* eslint no-console: 0 */
-
-const ZenHub = require( 'zenhub-api' );
-const { REPO, pkg } = require( '../config' );
-const { authedGraphql } = require( '../common' );
-const { pull } = require( 'lodash' );
-
-const api = new ZenHub( pkg.changelog.zhApiToken );
-
-const getQuery = ( before ) => {
- const [ owner, repo ] = REPO.split( '/' );
- const paging = before ? `, before: "${ before }"` : '';
- const query = `
- {
- repository(owner: "${ owner }", name: "${ repo }") {
- pullRequests(last: 100, states: [MERGED]${ paging }) {
- totalCount
- pageInfo {
- startCursor
- }
- nodes {
- number
- title
- url
- author {
- login
- }
- body
- labels(last: 10) {
- nodes {
- name
- }
- }
- }
- }
- }
- }
- `;
- return query;
-};
-
-const fetchAllIssuesForRelease = async ( releaseId ) => {
- const releaseIssues = await api.getReleaseReportIssues( {
- release_id: releaseId,
- } );
- return releaseIssues.map( ( releaseIssue ) => releaseIssue.issue_number );
-};
-
-const extractPullRequestsMatchingReleaseIssue = (
- releaseIds,
- pullRequests
-) => {
- return pullRequests.filter( ( pullRequest ) => {
- const hasPullRequest = releaseIds.includes( pullRequest.number );
- if ( hasPullRequest ) {
- pull( releaseIds, pullRequest.number );
- return true;
- }
- return false;
- } );
-};
-
-const fetchAllPullRequests = async ( releaseId ) => {
- // first get all release issue ids
- const releaseIds = await fetchAllIssuesForRelease( releaseId );
- let maxPages = Math.ceil( releaseIds.length / 100 ) + 2;
- const fetchResults = async ( before ) => {
- const query = getQuery( before );
- const results = await authedGraphql( query );
- const pullRequests = extractPullRequestsMatchingReleaseIssue(
- releaseIds,
- results.repository.pullRequests.nodes
- );
- if ( maxPages === 0 ) {
- return pullRequests;
- }
- maxPages--;
- const nextResults = await fetchResults(
- results.repository.pullRequests.pageInfo.startCursor
- );
- return pullRequests.concat(
- extractPullRequestsMatchingReleaseIssue( releaseIds, nextResults )
- );
- };
- let results = [];
- try {
- results = await fetchResults();
- } catch ( e ) {
- console.log( e.request );
- console.log( e.message );
- console.log( e.data );
- }
- return results;
-};
-
-module.exports = {
- fetchAllPullRequests,
-};
diff --git a/plugins/woocommerce-blocks/bin/copy-plugin-files.sh b/plugins/woocommerce-blocks/bin/copy-plugin-files.sh
deleted file mode 100644
index a4921af31f4..00000000000
--- a/plugins/woocommerce-blocks/bin/copy-plugin-files.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-cd "$1" || exit
-rsync ./ "$2"/ --recursive --delete --delete-excluded \
- --exclude=assets/ \
- --exclude=".*/" \
- --exclude="*.md" \
- --exclude=".*" \
- --exclude="composer.*" \
- --exclude="*.lock" \
- --exclude=bin/ \
- --exclude=node_modules/ \
- --exclude=tests/ \
- --exclude=docs/ \
- --exclude=phpcs.xml \
- --exclude=phpunit.xml.dist \
- --exclude=CODEOWNERS \
- --exclude=renovate.json \
- --exclude="*.config.js" \
- --exclude="*-config.js" \
- --exclude="*.config.json" \
- --exclude=package.json \
- --exclude=package-lock.json \
- --exclude=none \
- --exclude=blocks.ini \
- --exclude=docker-compose.yml \
- --exclude=tsconfig.json \
- --exclude=tsconfig.base.json \
- --exclude=woocommerce-gutenberg-products-block.zip \
- --exclude="zip-file/" \
- --exclude=global.d.ts \
- --exclude=packages/ \
- --exclude=patches/ \
- --exclude=reports/ \
- --exclude=storybook/
-echo -e "\nDone copying files!\n"
diff --git a/plugins/woocommerce-blocks/bin/development-check.sh b/plugins/woocommerce-blocks/bin/development-check.sh
deleted file mode 100755
index 0514bdea938..00000000000
--- a/plugins/woocommerce-blocks/bin/development-check.sh
+++ /dev/null
@@ -1,83 +0,0 @@
-#!/bin/bash
-
-# Output colorized strings
-#
-# Color codes:
-# 0 - black
-# 1 - red
-# 2 - green
-# 3 - yellow
-# 4 - blue
-# 5 - magenta
-# 6 - cian
-# 7 - white
-output() {
- echo "$(tput setaf "$1")$2$(tput sgr0)"
-}
-
-pass() {
- output 2 "$1"
-}
-
-fail() {
- output 1 "$1"
-}
-
-warn() {
- output 3 "$1"
-}
-
-function command_exists_as_alias {
- alias $1 2>/dev/null >/dev/null
-}
-
-function which {
- type "$1" >>/dev/null 2>&1
-}
-
-function command_is_available {
- which $1 || command_exists_as_alias $1 || type $1 >/dev/null 2>/dev/null
-}
-
-function node_modules_are_available {
- [ -d node_modules ]
-}
-
-function vendor_dir_is_available {
- [ -d vendor ]
-}
-
-function assert {
- $1 $2 && pass "- $3 is available ✔"
- $1 $2 || fail "- $3 is missing ✗ $4"
-}
-
-echo
-output 6 "BLOCKS DEVELOPMENT ENVIRONMENT CHECKS"
-output 6 "====================================="
-echo
-echo "Checking under $PWD"
-echo
-output 6 "(*・‿・)ノ⌒*:・゚✧"
-
-echo
-echo "Tools for building assets"
-echo "========================="
-echo
-assert command_is_available node "Node.js" "Node and NPM allow us to install required dependencies. You can install it from here: https://nodejs.org/en/download/"
-assert command_is_available composer "Composer" "Composer allows us to install PHP dependencies. You can install it from https://getcomposer.org, or if you are running Brew you can install it by running $ brew install composer"
-
-echo
-echo "Dependencies"
-echo "============"
-echo
-assert node_modules_are_available "" "node_modules dir" "You need to have node installed and run: $ npm install"
-assert vendor_dir_is_available "" "vendor dir" "You need to have composer installed and run: $ composer install"
-
-echo
-echo "Contributing and other helpers"
-echo "=============================="
-echo
-assert command_is_available git "git" "Git is required to push and pull from the GitHub repository. If you're running Brew, you can install it by running $ brew install git"
-assert command_is_available hub "Hub" "Hub provides some useful git commands used by the deployment scripts. If you're running Brew, you can install it by running $ brew install hub"
-echo
diff --git a/plugins/woocommerce-blocks/bin/fix-package-lock.sh b/plugins/woocommerce-blocks/bin/fix-package-lock.sh
deleted file mode 100755
index 64471875e33..00000000000
--- a/plugins/woocommerce-blocks/bin/fix-package-lock.sh
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/bin/bash
-
-# Enable nicer messaging for build status.
-BLUE_BOLD='\033[1;34m';
-RED_BOLD='\033[1;31m';
-COLOR_RESET='\033[0m';
-GREEN_BOLD='\033[1;32m';
-RED_BOLD='\033[1;31m';
-YELLOW_BOLD='\033[1;33m';
-error () {
- echo -e "${RED_BOLD}$1${COLOR_RESET}\n";
- exit 0;
-}
-status () {
- echo -e "${BLUE_BOLD}$1${COLOR_RESET}\n"
-}
-success () {
- echo -e "${GREEN_BOLD}$1${COLOR_RESET}\n"
-}
-warning () {
- echo -e "${YELLOW_BOLD}$1${COLOR_RESET}\n"
-}
-
-[[ -z "$1" ]] && {
- error "You must specify a branch to fix, for example: npm run fix-package-lock your/branch";
-}
-
-echo -e "${YELLOW_BOLD} ___ ___ ___
-| | | |
-|___|___|___|
-| | | |
-|___|___|___|
-| | | |
-|___|___|___|
-
-FIX PACKAGE LOCK
-================
-This script will attempt to rebase a Renovate PR and update the package.lock file.
-Usage: npm run fix-package-lock branch/name
-${COLOR_RESET}"
-
-echo -e "${RED_BOLD}BEFORE PROCEEDING\n=================
-You should check the PR on GitHub to see if it already has conflicts with trunk.
-If it does, use the checkbox in the PR to force Renovate to rebase it for you.
-Once the PR has been rebased, you can run this script, and then do a squash merge on GitHub.${COLOR_RESET}"
-
-printf "Ready to proceed? [y/N]: "
-read -r PROCEED
-echo
-
-if [ "$(echo "${PROCEED:-n}" | tr "[:upper:]" "[:lower:]")" != "y" ]; then
- exit
-fi
-
-git fetch
-
-if ! git checkout $1
-then
- error "Unable to checkout branch";
-else
- success "Checked out branch"
-fi
-
-status "Removing package-lock.json...";
-rm package-lock.json
-
-status "Installing dependencies...";
-npm cache verify
-npm install
-
-status "Comitting updated package-lock.json...";
-git add package-lock.json
-git commit -m 'update package-lock.json'
-git push --force-with-lease
-
-success "Done. Package Lock has been updated. 🎉"
diff --git a/plugins/woocommerce-blocks/bin/webpack-configs.js b/plugins/woocommerce-blocks/bin/webpack-configs.js
index 6ad91380df0..532ad61a231 100644
--- a/plugins/woocommerce-blocks/bin/webpack-configs.js
+++ b/plugins/woocommerce-blocks/bin/webpack-configs.js
@@ -28,6 +28,7 @@ const {
getProgressBarPluginConfig,
getCacheGroups,
} = require( './webpack-helpers' );
+const AddSplitChunkDependencies = require( './add-split-chunk-dependencies' );
const isProduction = NODE_ENV === 'production';
@@ -42,7 +43,7 @@ const getSharedPlugins = ( {
[
CHECK_CIRCULAR_DEPS === 'true' && checkCircularDeps !== false
? new CircularDependencyPlugin( {
- exclude: /node_modules/,
+ exclude: [ /[\/\\](node_modules|build|docs|vendor)[\/\\]/ ],
cwd: process.cwd(),
failOnError: 'warn',
} )
@@ -93,7 +94,9 @@ const getCoreConfig = ( options = {} ) => {
rules: [
{
test: /\.(t|j)sx?$/,
- exclude: /node_modules/,
+ exclude: [
+ /[\/\\](node_modules|build|docs|bin|storybook|tests|test)[\/\\]/,
+ ],
use: {
loader: 'babel-loader',
options: {
@@ -102,6 +105,11 @@ const getCoreConfig = ( options = {} ) => {
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-class-properties',
],
+ cacheDirectory: path.resolve(
+ __dirname,
+ '../../../node_modules/.cache/babel-loader'
+ ),
+ cacheCompression: false,
},
},
},
@@ -202,7 +210,7 @@ const getMainConfig = ( options = {} ) => {
rules: [
{
test: /\.(j|t)sx?$/,
- exclude: /node_modules/,
+ exclude: [ /[\/\\](node_modules|build|docs|vendor)[\/\\]/ ],
use: {
loader: 'babel-loader',
options: {
@@ -216,7 +224,11 @@ const getMainConfig = ( options = {} ) => {
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-class-properties',
].filter( Boolean ),
- cacheDirectory: true,
+ cacheDirectory: path.resolve(
+ __dirname,
+ '../../../node_modules/.cache/babel-loader'
+ ),
+ cacheCompression: false,
},
},
},
@@ -329,18 +341,7 @@ const getFrontConfig = ( options = {} ) => {
// translations which we must avoid.
// @see https://github.com/Automattic/jetpack/pull/20926
chunkFilename: `[name]-frontend${ fileSuffix }.js?ver=[contenthash]`,
- filename: ( pathData ) => {
- // blocksCheckout and blocksComponents were moved from core bundle,
- // retain their filenames to avoid breaking translations.
- if (
- pathData.chunk.name === 'blocksCheckout' ||
- pathData.chunk.name === 'blocksComponents'
- ) {
- return `${ paramCase(
- pathData.chunk.name
- ) }${ fileSuffix }.js`;
- }
-
+ filename: () => {
return `[name]-frontend${ fileSuffix }.js`;
},
uniqueName: 'webpackWcBlocksFrontendJsonp',
@@ -350,7 +351,7 @@ const getFrontConfig = ( options = {} ) => {
rules: [
{
test: /\.(j|t)sx?$/,
- exclude: /node_modules/,
+ exclude: [ /[\/\\](node_modules|build|docs|vendor)[\/\\]/ ],
use: {
loader: 'babel-loader',
options: {
@@ -376,7 +377,11 @@ const getFrontConfig = ( options = {} ) => {
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-class-properties',
].filter( Boolean ),
- cacheDirectory: true,
+ cacheDirectory: path.resolve(
+ __dirname,
+ '../../../node_modules/.cache/babel-loader'
+ ),
+ cacheCompression: false,
},
},
},
@@ -395,9 +400,10 @@ const getFrontConfig = ( options = {} ) => {
minSize: 200000,
automaticNameDelimiter: '--',
cacheGroups: {
- commons: {
+ vendor: {
test: /[\\/]node_modules[\\/]/,
- name: 'wc-blocks-vendors',
+ // Note that filenames are suffixed with `frontend` so the generated file is `wc-blocks-frontend-vendors-frontend`.
+ name: 'wc-blocks-frontend-vendors',
chunks: ( chunk ) => {
return (
chunk.name !== 'product-button-interactivity'
@@ -431,6 +437,7 @@ const getFrontConfig = ( options = {} ) => {
bundleAnalyzerReportTitle: 'Frontend',
} ),
new ProgressBarPlugin( getProgressBarPluginConfig( 'Frontend' ) ),
+ new AddSplitChunkDependencies(),
],
resolve: {
...resolve,
@@ -466,7 +473,7 @@ const getPaymentsConfig = ( options = {} ) => {
rules: [
{
test: /\.(j|t)sx?$/,
- exclude: /node_modules/,
+ exclude: [ /[\/\\](node_modules|build|docs|vendor)[\/\\]/ ],
use: {
loader: 'babel-loader',
options: {
@@ -492,7 +499,11 @@ const getPaymentsConfig = ( options = {} ) => {
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-class-properties',
].filter( Boolean ),
- cacheDirectory: true,
+ cacheDirectory: path.resolve(
+ __dirname,
+ '../../../node_modules/.cache/babel-loader'
+ ),
+ cacheCompression: false,
},
},
},
@@ -573,7 +584,7 @@ const getExtensionsConfig = ( options = {} ) => {
rules: [
{
test: /\.(j|t)sx?$/,
- exclude: /node_modules/,
+ exclude: [ /[\/\\](node_modules|build|docs|vendor)[\/\\]/ ],
use: {
loader: 'babel-loader',
options: {
@@ -599,7 +610,11 @@ const getExtensionsConfig = ( options = {} ) => {
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-class-properties',
].filter( Boolean ),
- cacheDirectory: true,
+ cacheDirectory: path.resolve(
+ __dirname,
+ '../../../node_modules/.cache/babel-loader'
+ ),
+ cacheCompression: false,
},
},
},
@@ -680,9 +695,9 @@ const getSiteEditorConfig = ( options = {} ) => {
rules: [
{
test: /\.(j|t)sx?$/,
- exclude: /node_modules/,
+ exclude: [ /[\/\\](node_modules|build|docs|vendor)[\/\\]/ ],
use: {
- loader: 'babel-loader?cacheDirectory',
+ loader: 'babel-loader',
options: {
presets: [
[
@@ -705,6 +720,11 @@ const getSiteEditorConfig = ( options = {} ) => {
: false,
'@babel/plugin-proposal-optional-chaining',
].filter( Boolean ),
+ cacheDirectory: path.resolve(
+ __dirname,
+ '../../../node_modules/.cache/babel-loader'
+ ),
+ cacheCompression: false,
},
},
},
@@ -838,8 +858,9 @@ const getStylingConfig = ( options = {} ) => {
rules: [
{
test: /\.(j|t)sx?$/,
+ exclude: [ /[\/\\](node_modules|build|docs|vendor)[\/\\]/ ],
use: {
- loader: 'babel-loader?cacheDirectory',
+ loader: 'babel-loader',
options: {
presets: [ '@wordpress/babel-preset-default' ],
plugins: [
@@ -851,6 +872,11 @@ const getStylingConfig = ( options = {} ) => {
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-class-properties',
].filter( Boolean ),
+ cacheDirectory: path.resolve(
+ __dirname,
+ '../../../node_modules/.cache/babel-loader'
+ ),
+ cacheCompression: false,
},
},
},
@@ -953,13 +979,11 @@ const getInteractivityAPIConfig = ( options = {} ) => {
rules: [
{
test: /\.(j|t)sx?$/,
- exclude: /node_modules/,
+ exclude: [ /[\/\\](node_modules|build|docs|vendor)[\/\\]/ ],
use: [
{
loader: require.resolve( 'babel-loader' ),
options: {
- cacheDirectory:
- process.env.BABEL_CACHE_DIRECTORY || true,
babelrc: false,
configFile: false,
presets: [
@@ -977,6 +1001,11 @@ const getInteractivityAPIConfig = ( options = {} ) => {
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-class-properties',
],
+ cacheDirectory: path.resolve(
+ __dirname,
+ '../../../node_modules/.cache/babel-loader'
+ ),
+ cacheCompression: false,
},
},
],
@@ -986,6 +1015,155 @@ const getInteractivityAPIConfig = ( options = {} ) => {
};
};
+const getCartAndCheckoutFrontendConfig = ( options = {} ) => {
+ let { fileSuffix } = options;
+ const { alias, resolvePlugins = [] } = options;
+ fileSuffix = fileSuffix ? `-${ fileSuffix }` : '';
+
+ const resolve = alias
+ ? {
+ alias,
+ plugins: resolvePlugins,
+ }
+ : {
+ plugins: resolvePlugins,
+ };
+ return {
+ entry: getEntryConfig(
+ 'cartAndCheckoutFrontend',
+ options.exclude || []
+ ),
+ output: {
+ devtoolNamespace: 'wc',
+ path: path.resolve( __dirname, '../build/' ),
+ // This is a cache busting mechanism which ensures that the script is loaded via the browser with a ?ver=hash
+ // string. The hash is based on the built file contents.
+ // @see https://github.com/webpack/webpack/issues/2329
+ // Using the ?ver string is needed here so the filename does not change between builds. The WordPress
+ // i18n system relies on the hash of the filename, so changing that frequently would result in broken
+ // translations which we must avoid.
+ // @see https://github.com/Automattic/jetpack/pull/20926
+ chunkFilename: `[name]-frontend${ fileSuffix }.js?ver=[contenthash]`,
+ filename: ( pathData ) => {
+ // blocksCheckout and blocksComponents were moved from core bundle,
+ // retain their filenames to avoid breaking translations.
+ if (
+ pathData.chunk.name === 'blocksCheckout' ||
+ pathData.chunk.name === 'blocksComponents'
+ ) {
+ return `${ paramCase(
+ pathData.chunk.name
+ ) }${ fileSuffix }.js`;
+ }
+
+ return `[name]-frontend${ fileSuffix }.js`;
+ },
+ uniqueName: 'webpackWcBlocksCartCheckoutFrontendJsonp',
+ library: [ 'wc', '[name]' ],
+ },
+ module: {
+ rules: [
+ {
+ test: /\.(j|t)sx?$/,
+ exclude: [ /[\/\\](node_modules|build|docs|vendor)[\/\\]/ ],
+ use: {
+ loader: 'babel-loader',
+ options: {
+ presets: [
+ [
+ '@wordpress/babel-preset-default',
+ {
+ modules: false,
+ targets: {
+ browsers: [
+ 'extends @wordpress/browserslist-config',
+ ],
+ },
+ },
+ ],
+ ],
+ plugins: [
+ isProduction
+ ? require.resolve(
+ 'babel-plugin-transform-react-remove-prop-types'
+ )
+ : false,
+ '@babel/plugin-proposal-optional-chaining',
+ '@babel/plugin-proposal-class-properties',
+ ].filter( Boolean ),
+ cacheDirectory: path.resolve(
+ __dirname,
+ '../../../node_modules/.cache/babel-loader'
+ ),
+ cacheCompression: false,
+ },
+ },
+ },
+ {
+ test: /\.s[c|a]ss$/,
+ use: {
+ loader: 'ignore-loader',
+ },
+ },
+ ],
+ },
+ optimization: {
+ concatenateModules:
+ isProduction && ! process.env.WP_BUNDLE_ANALYZER,
+ splitChunks: {
+ minSize: 200000,
+ automaticNameDelimiter: '--',
+ cacheGroups: {
+ commons: {
+ test: /[\\/]node_modules[\\/]/,
+ name: 'wc-cart-checkout-vendors',
+ chunks: 'all',
+ enforce: true,
+ },
+ base: {
+ // A refined include blocks and settings that are shared between cart and checkout that produces the smallest possible bundle.
+ test: /assets[\\/]js[\\/](settings|previews|base|data|utils|blocks[\\/]cart-checkout-shared|icons)|packages[\\/](checkout|components)|atomic[\\/]utils/,
+ name: 'wc-cart-checkout-base',
+ chunks: 'all',
+ enforce: true,
+ },
+ ...getCacheGroups(),
+ },
+ },
+ minimizer: [
+ new TerserPlugin( {
+ parallel: true,
+ terserOptions: {
+ output: {
+ comments: /translators:/i,
+ },
+ compress: {
+ passes: 2,
+ },
+ mangle: {
+ reserved: [ '__', '_n', '_nx', '_x' ],
+ },
+ },
+ extractComments: false,
+ } ),
+ ],
+ },
+ plugins: [
+ ...getSharedPlugins( {
+ bundleAnalyzerReportTitle: 'Cart & Checkout Frontend',
+ } ),
+ new ProgressBarPlugin(
+ getProgressBarPluginConfig( 'Cart & Checkout Frontend' )
+ ),
+ new AddSplitChunkDependencies(),
+ ],
+ resolve: {
+ ...resolve,
+ extensions: [ '.js', '.ts', '.tsx' ],
+ },
+ };
+};
+
module.exports = {
getCoreConfig,
getFrontConfig,
@@ -995,4 +1173,5 @@ module.exports = {
getSiteEditorConfig,
getStylingConfig,
getInteractivityAPIConfig,
+ getCartAndCheckoutFrontendConfig,
};
diff --git a/plugins/woocommerce-blocks/bin/webpack-entries.js b/plugins/woocommerce-blocks/bin/webpack-entries.js
index 76256a9a054..be936fafb02 100644
--- a/plugins/woocommerce-blocks/bin/webpack-entries.js
+++ b/plugins/woocommerce-blocks/bin/webpack-entries.js
@@ -25,9 +25,7 @@ const blocks = {
},
'attribute-filter': {},
breadcrumbs: {},
- cart: {},
'catalog-sorting': {},
- checkout: {},
'coming-soon': {},
'customer-account': {},
'featured-category': {
@@ -43,10 +41,6 @@ const blocks = {
customDir: 'classic-template',
},
'classic-shortcode': {},
- 'mini-cart': {},
- 'mini-cart-contents': {
- customDir: 'mini-cart/mini-cart-contents',
- },
'store-notices': {},
'page-content-wrapper': {},
'price-filter': {},
@@ -169,12 +163,22 @@ const blocks = {
},
};
+// Intentional separation of cart and checkout entry points to allow for better code splitting.
+const cartAndCheckoutBlocks = {
+ cart: {},
+ checkout: {},
+ 'mini-cart': {},
+ 'mini-cart-contents': {
+ customDir: 'mini-cart/mini-cart-contents',
+ },
+};
+
// Returns the entries for each block given a relative path (ie: `index.js`,
// `**/*.scss`...).
// It also filters out elements with undefined props and experimental blocks.
-const getBlockEntries = ( relativePath ) => {
+const getBlockEntries = ( relativePath, blockEntries = blocks ) => {
return Object.fromEntries(
- Object.entries( blocks )
+ Object.entries( blockEntries )
.map( ( [ blockCode, config ] ) => {
const filePaths = glob.sync(
`./assets/js/blocks/${ config.customDir || blockCode }/` +
@@ -206,7 +210,10 @@ const entries = {
'./assets/js/atomic/blocks/product-elements/product-details/index.tsx',
'add-to-cart-form':
'./assets/js/atomic/blocks/product-elements/add-to-cart-form/index.tsx',
- ...getBlockEntries( '{index,block,frontend}.{t,j}s{,x}' ),
+ ...getBlockEntries( '{index,block,frontend}.{t,j}s{,x}', {
+ ...blocks,
+ ...cartAndCheckoutBlocks,
+ } ),
// Interactivity component styling
'wc-interactivity-checkbox-list':
@@ -239,17 +246,14 @@ const entries = {
'wc-blocks': './assets/js/index.js',
// Blocks
- ...getBlockEntries( 'index.{t,j}s{,x}' ),
+ ...getBlockEntries( 'index.{t,j}s{,x}', {
+ ...blocks,
+ ...cartAndCheckoutBlocks,
+ } ),
},
frontend: {
reviews: './assets/js/blocks/reviews/frontend.ts',
...getBlockEntries( 'frontend.{t,j}s{,x}' ),
-
- blocksCheckout: './packages/checkout/index.js',
- blocksComponents: './packages/components/index.ts',
-
- 'mini-cart-component':
- './assets/js/blocks/mini-cart/component-frontend.tsx',
'product-button-interactivity':
'./assets/js/atomic/blocks/product-elements/button/frontend.tsx',
},
@@ -273,6 +277,13 @@ const entries = {
'wc-blocks-classic-template-revert-button':
'./assets/js/templates/revert-button/index.tsx',
},
+ cartAndCheckoutFrontend: {
+ ...getBlockEntries( 'frontend.{t,j}s{,x}', cartAndCheckoutBlocks ),
+ blocksCheckout: './packages/checkout/index.js',
+ blocksComponents: './packages/components/index.ts',
+ 'mini-cart-component':
+ './assets/js/blocks/mini-cart/component-frontend.tsx',
+ },
};
const getEntryConfig = ( type = 'main', exclude = [] ) => {
diff --git a/plugins/woocommerce-blocks/bin/wordpress-deploy.sh b/plugins/woocommerce-blocks/bin/wordpress-deploy.sh
deleted file mode 100644
index 5b24807f010..00000000000
--- a/plugins/woocommerce-blocks/bin/wordpress-deploy.sh
+++ /dev/null
@@ -1,201 +0,0 @@
-#!/bin/sh
-
-RELEASER_PATH=$(pwd)
-PLUGIN_SLUG="woo-gutenberg-products-block"
-GITHUB_ORG="woocommerce"
-GITHUB_SLUG="woocommerce-gutenberg-products-block"
-IS_PRE_RELEASE=false
-BUILD_PATH="${HOME}/blocks-deployment"
-
-# When it is set to true, the commands are just printed but not executed.
-DRY_RUN_MODE=false
-
-# When it is set to true, the commands that affect the local env are executed (e.g. git commit), while the commands that affect the remote env are not executed but just printed (e.g. git push)
-SIMULATE_RELEASE_MODE=false
-
-# Functions
-# Check if string contains substring
-is_substring() {
- case "$2" in
- *$1*)
- return 0
- ;;
- *)
- return 1
- ;;
- esac
-}
-
-# Output colorized strings
-#
-# Color codes:
-# 0 - black
-# 1 - red
-# 2 - green
-# 3 - yellow
-# 4 - blue
-# 5 - magenta
-# 6 - cian
-# 7 - white
-output() {
- echo "$(tput setaf "$1")$2$(tput sgr0)"
-}
-
-# Output colorized list
-output_list() {
- echo "$(tput setaf "$1") • $2:$(tput sgr0) \"$3\""
-}
-
-simulate() {
- if $2 = true ; then
- eval "$1"
- else
- output 3 "DRY RUN: $1"
- fi
-}
-
-
-run_command() {
- if $DRY_RUN_MODE = true; then
- output 3 "DRY RUN: $1"
- elif $SIMULATE_RELEASE_MODE = true; then
- simulate "$1" $2
- else
- eval "$1"
- fi
-}
-
-# Release script
-echo
-output 4 "BLOCKS->WordPress.org RELEASE SCRIPT"
-output 4 "===================================="
-echo
-printf "This script prepares a GitHub tag/release for WordPress.org SVN."
-echo
-echo
-echo "Before proceeding:"
-echo " • Ensure you have already created the release on GitHub. You can use '$ npm run deploy'."
-echo
-output 3 "Do you want to continue? [y/N]: "
-read -r PROCEED
-echo
-
-if [ "$(echo "${PROCEED:-n}" | tr "[:upper:]" "[:lower:]")" != "y" ]; then
- output 1 "Release cancelled!"
- exit 1
-fi
-echo
-output 3 "Please enter the version number to tag, for example, 1.0.0:"
-read -r VERSION
-echo
-
-# Check if is a pre-release.
-if is_substring "-" "${VERSION}"; then
- IS_PRE_RELEASE=true
- output 2 "Detected pre-release version!"
-fi
-
-# Set deploy variables
-SVN_REPO="http://plugins.svn.wordpress.org/${PLUGIN_SLUG}/"
-GIT_REPO="https://github.com/${GITHUB_ORG}/${GITHUB_SLUG}.git"
-SVN_PATH="${BUILD_PATH}/${PLUGIN_SLUG}-svn"
-GIT_PATH="${BUILD_PATH}/${PLUGIN_SLUG}-git"
-BRANCH="v$VERSION"
-
-echo
-echo "-------------------------------------------"
-echo
-echo "Review all data before proceeding:"
-echo
-output_list 3 "Version to release" "${VERSION}"
-output_list 3 "GIT tag to release" "${BRANCH}"
-output_list 3 "GIT repository" "${GIT_REPO}"
-output_list 3 "wp.org repository" "${SVN_REPO}"
-echo
-output 3 "Do you want to continue? [y/N]: "
-read -r PROCEED
-echo
-
-if [ "$(echo "${PROCEED:-n}" | tr "[:upper:]" "[:lower:]")" != "y" ]; then
- output 1 "Release cancelled!"
- exit 1
-fi
-
-# Create build directory if does not exists
-if [ ! -d "$BUILD_PATH" ]; then
- mkdir -p "$BUILD_PATH"
-fi
-
-# Delete old GIT directory
-rm -rf "$GIT_PATH"
-
-# Clone GIT repository
-output 2 "Cloning GIT repository..."
-run_command "git clone '$GIT_REPO' '$GIT_PATH' --branch '$BRANCH' --single-branch || exit '$?'" true
-
-if [ ! -d "$GIT_PATH/build" ]; then
- output 3 "Build directory not found in tag. Aborting."
- exit 1
-fi
-
-if [ ! -d "$GIT_PATH/vendor" ]; then
- output 3 "Vendor directory not found in tag. Aborting."
- exit 1
-fi
-
-# Checkout SVN repository if not exists
-if [ ! -d "$SVN_PATH" ]; then
- output 2 "No SVN directory found, fetching files..."
- # Checkout project without any file
- run_command "svn co --depth=files '$SVN_REPO' '$SVN_PATH'" true
-
- cd "$SVN_PATH" || exit
-
- # Fetch main directories
- run_command "svn up assets branches trunk" true
-
- # Fetch tags directories without content
- run_command "svn up --set-depth=immediates tags" true
- # To fetch content for a tag, use:
- # svn up --set-depth=infinity tags/
-else
- # Update SVN
- cd "$SVN_PATH" || exit
- output 2 "Updating SVN..."
- run_command "svn up" true
-fi
-
-# Copy GIT directory to trunk
-output 2 "Copying project files to SVN trunk..."
-run_command "sh '${RELEASER_PATH}/bin/copy-plugin-files.sh' '$GIT_PATH' '$SVN_PATH/trunk'" true
-cd "$SVN_PATH"
-
-# Update stable tag on trunk/readme.txt
-if [ $IS_PRE_RELEASE = false ]; then
- output 2 "Updating \"Stable tag\" to ${VERSION} on trunk/readme.txt..."
- run_command "perl -i -pe's/Stable tag: .*/Stable tag: ${VERSION}/' trunk/readme.txt" true
-fi
-
-# Do the remove all deleted files
-run_command "svn st | grep -v '^.[ \t]*\..*' | grep '^\!' | awk '{print $2'@'}' | xargs svn rm" true
-
-# Do the add all not know files
-run_command "svn st | grep -v '^.[ \t]*\..*' | grep '^?' | awk '{print $2'@'}' | xargs svn add" true
-
-# Copy trunk to tag/$VERSION
-if [ ! -d "tags/${VERSION}" ]; then
- output 2 "Creating SVN tags/${VERSION}..."
- run_command "svn 'cp trunk tags/'${VERSION}''" true
-fi
-
-# Remove the GIT directory
-output 2 "Removing GIT directory..."
-run_command "rm -rf '$GIT_PATH'" true
-
-# SVN commit messsage
-output 2 "Ready to commit into WordPress.org Plugin's Directory!"
-echo
-echo "-------------------------------------------"
-echo
-output 3 "Run the following command to commit to SVN:"
-echo "cd ${SVN_PATH} && svn ci -m \"Release ${VERSION}, see readme.txt for changelog.\""
diff --git a/plugins/woocommerce-blocks/bin/wp-env-with-gutenberg.js b/plugins/woocommerce-blocks/bin/wp-env-with-gutenberg.js
deleted file mode 100644
index c3a6ea213ea..00000000000
--- a/plugins/woocommerce-blocks/bin/wp-env-with-gutenberg.js
+++ /dev/null
@@ -1,15 +0,0 @@
-const fs = require( 'fs' );
-const path = require( 'path' );
-
-const wpEnvRaw = fs.readFileSync(
- path.join( __dirname, '..', '.wp-env.json' )
-);
-const wpEnv = JSON.parse( wpEnvRaw );
-wpEnv.plugins.push(
- 'https://downloads.wordpress.org/plugin/gutenberg.latest-stable.zip'
-);
-// We write the new file to .wp-env.override.json (https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/#wp-env-override-json)
-fs.writeFileSync(
- path.join( __dirname, '..', '.wp-env.override.json' ),
- JSON.stringify( wpEnv )
-);
diff --git a/plugins/woocommerce-blocks/bin/wp-env-with-wp-641.js b/plugins/woocommerce-blocks/bin/wp-env-with-wp-641.js
deleted file mode 100755
index 377c3151503..00000000000
--- a/plugins/woocommerce-blocks/bin/wp-env-with-wp-641.js
+++ /dev/null
@@ -1,19 +0,0 @@
-const fs = require( 'fs' );
-const path = require( 'path' );
-
-const wpEnvRaw = fs.readFileSync(
- path.join( __dirname, '..', '.wp-env.json' )
-);
-const wpEnv = JSON.parse( wpEnvRaw );
-
-// Pin the core version to 6.2.2 for Jest E2E test so we can keep the test
-// passing when new WordPress versions are released. We do this because we're
-// moving to Playwright and will abandon the Jest E2E tests once the migration
-// is complete.
-wpEnv.core = 'WordPress/WordPress#6.4.1';
-
-// We write the new file to .wp-env.override.json (https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/#wp-env-override-json)
-fs.writeFileSync(
- path.join( __dirname, '..', '.wp-env.override.json' ),
- JSON.stringify( wpEnv )
-);
diff --git a/plugins/woocommerce-blocks/changelog/42008-add-interactive-active-filters-block b/plugins/woocommerce-blocks/changelog/42008-add-interactive-active-filters-block
deleted file mode 100644
index daafe4b9d00..00000000000
--- a/plugins/woocommerce-blocks/changelog/42008-add-interactive-active-filters-block
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: add
-Comment: Add new Active Filters block powered by Interactivity API.
-
diff --git a/plugins/woocommerce-blocks/changelog/42044-product-gallery-popup-simple b/plugins/woocommerce-blocks/changelog/42044-product-gallery-popup-simple
deleted file mode 100644
index 0329457f700..00000000000
--- a/plugins/woocommerce-blocks/changelog/42044-product-gallery-popup-simple
+++ /dev/null
@@ -1,3 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: Product Gallery: Simplify the Product Gallery Pop-up.
diff --git a/plugins/woocommerce-blocks/changelog/42719-product-gallery b/plugins/woocommerce-blocks/changelog/42719-product-gallery
deleted file mode 100644
index 163958d0d65..00000000000
--- a/plugins/woocommerce-blocks/changelog/42719-product-gallery
+++ /dev/null
@@ -1,3 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: Product Gallery: Add transforms for better discovery.
diff --git a/plugins/woocommerce-blocks/changelog/42724-update-42072-move-switch-classic-cart-checkout-button b/plugins/woocommerce-blocks/changelog/42724-update-42072-move-switch-classic-cart-checkout-button
deleted file mode 100644
index 098cff9a5db..00000000000
--- a/plugins/woocommerce-blocks/changelog/42724-update-42072-move-switch-classic-cart-checkout-button
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: enhancement
-
-Move the switch to classic shortcode block button to separate component.
\ No newline at end of file
diff --git a/plugins/woocommerce-blocks/changelog/42727-fix-blocks-linting b/plugins/woocommerce-blocks/changelog/42727-fix-blocks-linting
deleted file mode 100644
index 94175e5a77a..00000000000
--- a/plugins/woocommerce-blocks/changelog/42727-fix-blocks-linting
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Fixed linting errors
-
diff --git a/plugins/woocommerce-blocks/changelog/42751-update-42072-update-incompatibility-generic-notice b/plugins/woocommerce-blocks/changelog/42751-update-42072-update-incompatibility-generic-notice
deleted file mode 100644
index 38cc9009b8f..00000000000
--- a/plugins/woocommerce-blocks/changelog/42751-update-42072-update-incompatibility-generic-notice
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: enhancement
-
-Update the generic incompatibility notice message for the Cart and Checkout page.
\ No newline at end of file
diff --git a/plugins/woocommerce-blocks/changelog/42758-add-42133-custom-select-field b/plugins/woocommerce-blocks/changelog/42758-add-42133-custom-select-field
deleted file mode 100644
index 8bcddb48649..00000000000
--- a/plugins/woocommerce-blocks/changelog/42758-add-42133-custom-select-field
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: add
-
-Add support for select fields in the experimental WooCommerce Blocks custom fields API.
\ No newline at end of file
diff --git a/plugins/woocommerce-blocks/changelog/42798-fix-woocommerce-blocks-wp-env b/plugins/woocommerce-blocks/changelog/42798-fix-woocommerce-blocks-wp-env
deleted file mode 100644
index 9d5b2364d95..00000000000
--- a/plugins/woocommerce-blocks/changelog/42798-fix-woocommerce-blocks-wp-env
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Just a change to the `.wp-env` config.
-
diff --git a/plugins/woocommerce-blocks/changelog/43294-product-gallery-overlay-link-font-size b/plugins/woocommerce-blocks/changelog/43294-product-gallery-overlay-link-font-size
deleted file mode 100644
index 770d669e5d3..00000000000
--- a/plugins/woocommerce-blocks/changelog/43294-product-gallery-overlay-link-font-size
+++ /dev/null
@@ -1,3 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: Product Gallery: Use @container rule to adjust overlay link count font size.
diff --git a/plugins/woocommerce-blocks/changelog/43487-product-gallery-remove-default b/plugins/woocommerce-blocks/changelog/43487-product-gallery-remove-default
deleted file mode 100644
index d51608e0798..00000000000
--- a/plugins/woocommerce-blocks/changelog/43487-product-gallery-remove-default
+++ /dev/null
@@ -1,3 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: Remove Product Gallery (Beta) from being default.
diff --git a/plugins/woocommerce-blocks/changelog/add-custom-fields-for-checkout-block b/plugins/woocommerce-blocks/changelog/add-custom-fields-for-checkout-block
deleted file mode 100644
index c10f00cbb97..00000000000
--- a/plugins/woocommerce-blocks/changelog/add-custom-fields-for-checkout-block
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: add
-
-Add support for additional fields in Checkout block
diff --git a/plugins/woocommerce-blocks/changelog/add-product-gallery-block-group-block-labels b/plugins/woocommerce-blocks/changelog/add-product-gallery-block-group-block-labels
deleted file mode 100644
index ec93ce3aa02..00000000000
--- a/plugins/woocommerce-blocks/changelog/add-product-gallery-block-group-block-labels
+++ /dev/null
@@ -1,3 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: Product Gallery: Add Group block labels.
diff --git a/plugins/woocommerce-blocks/changelog/dev-cleanup-blocks-github-folder b/plugins/woocommerce-blocks/changelog/dev-cleanup-blocks-github-folder
deleted file mode 100644
index cf58fa76e5a..00000000000
--- a/plugins/woocommerce-blocks/changelog/dev-cleanup-blocks-github-folder
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-clear out unneeded github files from block folder
diff --git a/plugins/woocommerce-blocks/changelog/fix-41996 b/plugins/woocommerce-blocks/changelog/fix-41996
deleted file mode 100644
index 9a77d657c92..00000000000
--- a/plugins/woocommerce-blocks/changelog/fix-41996
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-update text domains in woocommerce-blocks folder
diff --git a/plugins/woocommerce-blocks/changelog/fix-42852-product-gallery-block-snapping-position-on-resize b/plugins/woocommerce-blocks/changelog/fix-42852-product-gallery-block-snapping-position-on-resize
deleted file mode 100644
index 8c2a08b82e2..00000000000
--- a/plugins/woocommerce-blocks/changelog/fix-42852-product-gallery-block-snapping-position-on-resize
+++ /dev/null
@@ -1,3 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: Product Gallery: Fix Large Image snapping position on window resize.
diff --git a/plugins/woocommerce-blocks/changelog/fix-43304-product-gallery-block-accessibility b/plugins/woocommerce-blocks/changelog/fix-43304-product-gallery-block-accessibility
deleted file mode 100644
index 88da937fbf4..00000000000
--- a/plugins/woocommerce-blocks/changelog/fix-43304-product-gallery-block-accessibility
+++ /dev/null
@@ -1,3 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: Product Gallery: Update div elements to ul and li for better accessibility.
diff --git a/plugins/woocommerce-blocks/changelog/fix-43306-product-gallery-pager-accessibility b/plugins/woocommerce-blocks/changelog/fix-43306-product-gallery-pager-accessibility
deleted file mode 100644
index 1195bc881ec..00000000000
--- a/plugins/woocommerce-blocks/changelog/fix-43306-product-gallery-pager-accessibility
+++ /dev/null
@@ -1,3 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: Product Gallery: Improve the accessibility of the Product Gallery Pager.
diff --git a/plugins/woocommerce-blocks/changelog/fix-ci-matrix b/plugins/woocommerce-blocks/changelog/fix-ci-matrix
deleted file mode 100644
index 6e630c233c9..00000000000
--- a/plugins/woocommerce-blocks/changelog/fix-ci-matrix
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-remove PHP related scripts from package.json
diff --git a/plugins/woocommerce-blocks/changelog/fix-domain-substitution b/plugins/woocommerce-blocks/changelog/fix-domain-substitution
deleted file mode 100644
index ca635aa1178..00000000000
--- a/plugins/woocommerce-blocks/changelog/fix-domain-substitution
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Add composer.json for live build
diff --git a/plugins/woocommerce-blocks/changelog/fix-html-tags-for-payment-description-42123 b/plugins/woocommerce-blocks/changelog/fix-html-tags-for-payment-description-42123
deleted file mode 100644
index ecda70b7577..00000000000
--- a/plugins/woocommerce-blocks/changelog/fix-html-tags-for-payment-description-42123
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Allow built in payment method descriptions to contain HTML when rendered on the block checkout.
diff --git a/plugins/woocommerce-blocks/changelog/product-gallery-variation-images b/plugins/woocommerce-blocks/changelog/product-gallery-variation-images
deleted file mode 100644
index 4699cd7665a..00000000000
--- a/plugins/woocommerce-blocks/changelog/product-gallery-variation-images
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Reset main image when variations are cleared.
-
diff --git a/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/checkout-block/additional-checkout-fields.md b/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/checkout-block/additional-checkout-fields.md
index 7e93e15487f..9e80fa42ab5 100644
--- a/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/checkout-block/additional-checkout-fields.md
+++ b/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/checkout-block/additional-checkout-fields.md
@@ -292,12 +292,15 @@ As well as the options above, text fields also support a `required` option. If t
As well as the options above, select fields must also be registered with an `options` option. This is used to specify what options the shopper can select.
-Select fields can also be marked as required. If they are not (i.e. they are optional), then an empty entry will be added to allow the shopper to unset the field.
+Select fields will mount with no value selected by default, if the field is required, the user will be required to select a value.
+
+You can set a placeholder to be shown on the select by passing a `placeholder` value when registering the field. This will be the first option in the select and will not be selectable if the field is required.
| Option name | Description | Required? | Example | Default value |
|-----|-----|-----|----------------|--------------|
| `options` | An array of options to show in the select input. Each options must be an array containing a `label` and `value` property. Each entry must have a unique `value`. Any duplicate options will be removed. The `value` is what gets submitted to the server during checkout and the `label` is simply a user-friendly representation of this value. It is not transmitted to the server in any way. | Yes | see below | No default - this must be provided. |
| `required` | If this is `true` then the shopper _must_ provide a value for this field during the checkout process. | No | `true` | `false` |
+| `placeholder` | If this value is set, the shopper will see this option in the select. If the select is required, the shopper cannot select this option. | No | `Select a role | Select a $label |
##### Example of `options` value
@@ -425,11 +428,12 @@ add_action(
function() {
woocommerce_register_additional_checkout_field(
array(
- 'id' => 'namespace/how-did-you-hear-about-us',
- 'label' => 'How did you hear about us?',
- 'location' => 'order',
- 'type' => 'select',
- 'options' => [
+ 'id' => 'namespace/how-did-you-hear-about-us',
+ 'label' => 'How did you hear about us?',
+ 'placeholder' => 'Select a source',
+ 'location' => 'order',
+ 'type' => 'select',
+ 'options' => [
[
'value' => 'google',
'label' => 'Google'
@@ -463,7 +467,7 @@ This results in the order information section being rendered like so:
![The select input when focused](https://github.com/woocommerce/woocommerce/assets/5656702/bd943906-621b-404f-aa84-b951323e25d3)
-If it is undesirable to force the shopper to select a value, providing a value such as "None of the above" may help.
+If it is undesirable to force the shopper to select a value, mark the select as optional by setting the `required` option to `false`.
## Validation and sanitization
diff --git a/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/register-product-collection.md b/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/register-product-collection.md
index fbfa68e5b2d..77a56533619 100644
--- a/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/register-product-collection.md
+++ b/plugins/woocommerce-blocks/docs/third-party-developers/extensibility/register-product-collection.md
@@ -66,7 +66,17 @@ Attributes are the properties that define the behavior of the collection. All th
- `type` (type `string`): The type of layout. Accepted values are `grid` and `stack`.
- `columns` (type `number`): The number of columns to display.
- `shrinkColumns` (type `boolean`): Whether the layout should be responsive.
-- `hideControls` (type `array`): The controls to hide.
+- `hideControls` (type `array`): The controls to hide. Possible values:
+ - `order` - "Order by" setting
+ - `attributes` - "Product Attributes" filter
+ - `created` - "Created" filter
+ - `featured` - "Featured" filter
+ - `hand-picked` - "Hand-picked Products" filter
+ - `keyword` - "Keyword" filter
+ - `on-sale` - "On Sale" filter
+ - `stock-status` - "Stock Status" filter
+ - `taxonomy` - "Product Categories", "Product Tags" and custom taxonomies filters
+ - `price-range` - "Price Range" filter
#### Preview Attribute
@@ -127,6 +137,7 @@ __experimentalRegisterProductCollection({
columns: 3,
shrinkColumns: true,
},
+ hideControls: [ "created", "stock-status" ]
},
});
```
@@ -225,8 +236,8 @@ This will create a collection with a heading, product image, and product price.
![image](https://github.com/woocommerce/woocommerce/assets/16707866/3d92c084-91e9-4872-a898-080b4b93afca)
-> ![TIP]
+> [!TIP]
> You can learn more about inner blocks template in the [Inner Blocks](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/nested-blocks-inner-blocks/#template) documentation.
-> ![TIP]
+> [!TIP]
> You can also take a look at how we are defining our core collections at `plugins/woocommerce-blocks/assets/js/blocks/product-collection/collections` directory. Our core collections will also evolve over time.
diff --git a/plugins/woocommerce-blocks/package.json b/plugins/woocommerce-blocks/package.json
index 48da6a8f1d6..d627512f2ff 100644
--- a/plugins/woocommerce-blocks/package.json
+++ b/plugins/woocommerce-blocks/package.json
@@ -54,8 +54,6 @@
"build:docs": "./vendor/bin/wp-hooks-generator --input=src --output=bin/hook-docs/data && node ./bin/hook-docs && pnpm build:docs:block-references",
"build:docs:block-references": "node ./bin/gen-block-list-doc.js",
"postbuild:docs": "./bin/add-doc-footer.sh",
- "changelog:zenhub": "node ./bin/changelog --changelogSrcType='ZENHUB_RELEASE'",
- "change-versions": "source ./bin/change-versions.sh",
"dev": "rimraf build/* && cross-env BABEL_ENV=default webpack",
"labels:dry": "github-label-sync --labels ./.github/label-sync-config.json --allow-added-labels --dry-run woocommerce/woocommerce-gutenberg-products-block",
"labels:sync": "github-label-sync --labels ./.github/label-sync-config.json --allow-added-labels woocommerce/woocommerce-gutenberg-products-block",
@@ -72,10 +70,8 @@
"lint:js:report": "pnpm run lint:js -- --output-file eslint_report.json --ext=js,ts,tsx --format json",
"lint:js-fix": "eslint assets/js --ext=js,jsx,ts,tsx --fix",
"lint:md:docs": "wp-scripts lint-md-docs",
- "fix-package-lock": "./bin/fix-package-lock.sh",
"pre-commit": "lint-staged",
"reformat-files": "prettier --ignore-path .eslintignore --write \"**/*.{js,jsx,json,ts,tsx}\"",
- "release": "sh ./bin/wordpress-deploy.sh",
"rimraf": "./node_modules/rimraf/bin.js",
"start": "rimraf build/* && cross-env BABEL_ENV=default CHECK_CIRCULAR_DEPS=true webpack --watch",
"storybook": "storybook dev -c ./storybook -p 6006 --ci",
@@ -87,7 +83,6 @@
"test:e2e:block-theme": "pnpm run test:e2e block_theme",
"test:e2e:classic-theme": "pnpm run test:e2e classic_theme",
"test:e2e:block-theme-with-templates": "pnpm run test:e2e block_theme_with_templates",
- "test:e2e:fake": "echo 'It should trigger Blocks e2e tests in @woocommerce/plugin-woocommerce'",
"test:e2e:jest": "pnpm run wp-env:config && cross-env JEST_PUPPETEER_CONFIG=tests/e2e-jest/config/jest-puppeteer.config.js NODE_CONFIG_DIR=tests/e2e-jest/config wp-scripts test-e2e --config tests/e2e-jest/config/jest.config.js",
"test:e2e:jest:dev": "pnpm run wp-env:config && cross-env JEST_PUPPETEER_CONFIG=tests/e2e-jest/config/jest-puppeteer.config-dev.js NODE_CONFIG_DIR=tests/e2e-jest/config wp-scripts test-e2e --config tests/e2e-jest/config/jest.config.js",
"test:e2e:jest:dev-watch": "pnpm run wp-env:config && cross-env JEST_PUPPETEER_CONFIG=tests/e2e-jest/config/jest-puppeteer.config-dev.js NODE_CONFIG_DIR=tests/e2e-jest/config wp-scripts test-e2e --config tests/e2e-jest/config/jest.config.js --watch",
@@ -108,8 +103,6 @@
"watch:build:project:bundle": "wireit"
},
"devDependencies": {
- "@actions/core": "1.10.0",
- "@actions/github": "5.1.1",
"@automattic/color-studio": "2.5.0",
"@babel/cli": "7.23.0",
"@babel/core": "7.23.2",
@@ -120,8 +113,6 @@
"@babel/preset-react": "7.23.3",
"@babel/preset-typescript": "7.23.2",
"@bartekbp/typescript-checkstyle": "5.0.0",
- "@octokit/action": "5.0.2",
- "@octokit/graphql": "5.0.5",
"@playwright/test": "^1.45.1",
"@storybook/addon-a11y": "7.5.2",
"@storybook/addon-actions": "^7.6.4",
@@ -162,7 +153,6 @@
"@types/wordpress__wordcount": "^2.4.5",
"@typescript-eslint/eslint-plugin": "5.56.0",
"@typescript-eslint/parser": "5.56.0",
- "@woocommerce/api": "0.2.0",
"@woocommerce/data": "workspace:*",
"@woocommerce/e2e-utils": "workspace:*",
"@woocommerce/eslint-plugin": "workspace:*",
@@ -196,14 +186,12 @@
"@wordpress/stylelint-config": "^21.36.0",
"allure-playwright": "^2.9.2",
"autoprefixer": "10.4.14",
- "axios": "0.27.2",
"babel-jest": "^29.7.0",
"babel-plugin-explicit-exports-references": "^1.0.2",
"babel-plugin-react-docgen": "4.2.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"chalk": "4.1.2",
"circular-dependency-plugin": "5.2.2",
- "commander": "11.0.0",
"copy-webpack-plugin": "11.0.0",
"core-js": "3.25.0",
"create-file-webpack": "1.0.2",
@@ -220,9 +208,6 @@
"eslint-plugin-storybook": "^0.6.15",
"eslint-plugin-woocommerce": "file:bin/eslint-plugin-woocommerce",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
- "expect-puppeteer": "6.1.1",
- "fast-xml-parser": "4.2.5",
- "fs-extra": "11.1.1",
"gh-pages": "4.0.0",
"github-label-sync": "^2.3.1",
"glob": "7.2.3",
@@ -238,15 +223,12 @@
"lint-staged": "13.2.0",
"lodash": "4.17.21",
"markdown-it": "13.0.1",
- "merge-config": "2.0.0",
"mini-css-extract-plugin": "2.7.6",
- "patch-package": "6.4.7",
"postcss": "8.4.32",
"postcss-color-function": "4.1.0",
"postcss-loader": "4.3.0",
"prettier": "npm:wp-prettier@^2.8.5",
"progress-bar-webpack-plugin": "2.1.0",
- "promptly": "3.2.0",
"puppeteer": "17.1.3",
"react-docgen": "5.4.3",
"react-docgen-typescript-plugin": "^1.0.5",
@@ -266,8 +248,7 @@
"webpack-bundle-analyzer": "4.7.0",
"webpack-cli": "5.1.4",
"wireit": "0.14.3",
- "wp-types": "3.63.0",
- "zenhub-api": "0.2.0"
+ "wp-types": "3.63.0"
},
"engines": {
"node": "^20.11.1",
@@ -294,6 +275,7 @@
"@wordpress/url": "3.13.0",
"@wordpress/wordcount": "3.47.0",
"change-case": "^4.1.2",
+ "check-password-strength": "^2.0.10",
"clsx": "^2.1.1",
"compare-versions": "4.1.3",
"config": "3.3.7",
@@ -373,17 +355,6 @@
"pull_request",
"push"
]
- },
- {
- "name": "Blocks e2e - trigger for Blocks e2e tests in @woocommerce/plugin-woocommerce",
- "testType": "e2e",
- "command": "test:e2e:fake",
- "changes": [
- "tests/e2e/**"
- ],
- "events": [
- "pull_request"
- ]
}
]
}
diff --git a/plugins/woocommerce-blocks/packages/checkout/utils/validation/get-validity-message-for-input.ts b/plugins/woocommerce-blocks/packages/checkout/utils/validation/get-validity-message-for-input.ts
index d5f658a4961..fda88d7e26b 100644
--- a/plugins/woocommerce-blocks/packages/checkout/utils/validation/get-validity-message-for-input.ts
+++ b/plugins/woocommerce-blocks/packages/checkout/utils/validation/get-validity-message-for-input.ts
@@ -3,6 +3,28 @@
*/
import { __, sprintf } from '@wordpress/i18n';
+const defaultValidityMessage =
+ ( label: string | undefined ) =>
+ ( validity: ValidityState ): string | undefined => {
+ const fieldLabel = label
+ ? label.toLowerCase()
+ : __( 'field', 'woocommerce' );
+
+ const invalidFieldMessage = sprintf(
+ /* translators: %s field label */
+ __( 'Please enter a valid %s', 'woocommerce' ),
+ fieldLabel
+ );
+
+ if (
+ validity.valueMissing ||
+ validity.badInput ||
+ validity.typeMismatch
+ ) {
+ return invalidFieldMessage;
+ }
+ };
+
/**
* Converts an input's validityState to a string to display on the frontend.
*
@@ -10,28 +32,22 @@ import { __, sprintf } from '@wordpress/i18n';
* could be implemented in the future but are not currently used by the block checkout).
*/
const getValidityMessageForInput = (
- label: string,
- inputElement: HTMLInputElement
+ label: string | undefined,
+ inputElement: HTMLInputElement,
+ customValidityMessage?: ( validity: ValidityState ) => string | undefined
): string => {
- const { valid, customError, valueMissing, badInput, typeMismatch } =
- inputElement.validity;
-
// No errors, or custom error - return early.
- if ( valid || customError ) {
+ if ( inputElement.validity.valid || inputElement.validity.customError ) {
return inputElement.validationMessage;
}
- const invalidFieldMessage = sprintf(
- /* translators: %s field label */
- __( 'Please enter a valid %s', 'woocommerce' ),
- label.toLowerCase()
+ const validityMessageCallback =
+ customValidityMessage || defaultValidityMessage( label );
+
+ return (
+ validityMessageCallback( inputElement.validity ) ||
+ inputElement.validationMessage
);
-
- if ( valueMissing || badInput || typeMismatch ) {
- return invalidFieldMessage;
- }
-
- return inputElement.validationMessage || invalidFieldMessage;
};
export default getValidityMessageForInput;
diff --git a/plugins/woocommerce-blocks/packages/components/checkbox-control/index.tsx b/plugins/woocommerce-blocks/packages/components/checkbox-control/index.tsx
index 65a84ad5e8a..ca531f79368 100644
--- a/plugins/woocommerce-blocks/packages/components/checkbox-control/index.tsx
+++ b/plugins/woocommerce-blocks/packages/components/checkbox-control/index.tsx
@@ -32,6 +32,8 @@ export const CheckboxControl = ( {
hasError = false,
checked = false,
disabled = false,
+ errorId,
+ errorMessage,
...rest
}: CheckboxControlProps & Record< string, unknown > ): JSX.Element => {
const instanceId = useInstanceId( CheckboxControl );
diff --git a/plugins/woocommerce-blocks/packages/components/checkbox-control/style.scss b/plugins/woocommerce-blocks/packages/components/checkbox-control/style.scss
index f7bfde6da55..ec864cfaf7b 100644
--- a/plugins/woocommerce-blocks/packages/components/checkbox-control/style.scss
+++ b/plugins/woocommerce-blocks/packages/components/checkbox-control/style.scss
@@ -2,6 +2,7 @@
@include reset-color();
@include reset-typography();
margin-top: em($gap);
+ line-height: 1;
label {
align-items: flex-start;
@@ -22,7 +23,6 @@
border: 1px solid $universal-border-medium;
border-radius: $universal-border-radius;
box-sizing: border-box;
- color: currentColor;
height: em(24px);
width: em(24px);
margin: 0;
@@ -35,11 +35,11 @@
background-color: #fff;
&:checked {
- border: 1px solid currentColor;
+ background: #fff;
}
&:focus {
- outline: 2px solid currentColor;
+ outline: 1px solid $universal-border-strong;
outline-offset: 2px;
}
@@ -89,7 +89,7 @@
}
.wc-block-components-checkbox__mark {
- fill: currentColor;
+ fill: #000;
position: absolute;
margin-left: em(3px);
margin-top: em(1px);
diff --git a/plugins/woocommerce-blocks/packages/components/formatted-monetary-amount/index.tsx b/plugins/woocommerce-blocks/packages/components/formatted-monetary-amount/index.tsx
index 74d76cdeb88..29f383896b9 100644
--- a/plugins/woocommerce-blocks/packages/components/formatted-monetary-amount/index.tsx
+++ b/plugins/woocommerce-blocks/packages/components/formatted-monetary-amount/index.tsx
@@ -9,6 +9,7 @@ import type {
import clsx from 'clsx';
import type { ReactElement } from 'react';
import type { Currency } from '@woocommerce/types';
+import { SITE_CURRENCY } from '@woocommerce/settings';
/**
* Internal dependencies
@@ -22,7 +23,7 @@ export interface FormattedMonetaryAmountProps
allowNegative?: boolean;
isAllowed?: ( formattedValue: NumberFormatValues ) => boolean;
value: number | string; // Value of money amount.
- currency: Currency | Record< string, never >; // Currency configuration object.
+ currency?: Currency | undefined; // Currency configuration object. Defaults to site currency.
onValueChange?: ( unit: number ) => void; // Function to call when value changes.
style?: React.CSSProperties | undefined;
renderText?: ( value: string ) => JSX.Element;
@@ -31,36 +32,25 @@ export interface FormattedMonetaryAmountProps
/**
* Formats currency data into the expected format for NumberFormat.
*/
-const currencyToNumberFormat = (
- currency: FormattedMonetaryAmountProps[ 'currency' ]
-) => {
- const hasSimiliarSeparators =
- currency?.thousandSeparator === currency?.decimalSeparator;
- if ( hasSimiliarSeparators ) {
+const currencyToNumberFormat = ( currency: Currency ) => {
+ const { prefix, suffix, thousandSeparator, decimalSeparator } = currency;
+ const hasDuplicateSeparator = thousandSeparator === decimalSeparator;
+ if ( hasDuplicateSeparator ) {
// eslint-disable-next-line no-console
console.warn(
'Thousand separator and decimal separator are the same. This may cause formatting issues.'
);
}
return {
- thousandSeparator: hasSimiliarSeparators
- ? ''
- : currency?.thousandSeparator,
- decimalSeparator: currency?.decimalSeparator,
+ thousandSeparator: hasDuplicateSeparator ? '' : thousandSeparator,
+ decimalSeparator,
fixedDecimalScale: true,
- prefix: currency?.prefix,
- suffix: currency?.suffix,
+ prefix,
+ suffix,
isNumericString: true,
};
};
-type CustomFormattedMonetaryAmountProps = Omit<
- FormattedMonetaryAmountProps,
- 'currency'
-> & {
- currency: Currency | Record< string, never >;
-};
-
/**
* FormattedMonetaryAmount component.
*
@@ -71,11 +61,18 @@ type CustomFormattedMonetaryAmountProps = Omit<
const FormattedMonetaryAmount = ( {
className,
value: rawValue,
- currency,
+ currency: rawCurrency = SITE_CURRENCY,
onValueChange,
displayType = 'text',
...props
-}: CustomFormattedMonetaryAmountProps ): ReactElement | null => {
+}: FormattedMonetaryAmountProps ): ReactElement | null => {
+ // Merge currency configuration with site currency.
+ const currency = {
+ ...SITE_CURRENCY,
+ ...rawCurrency,
+ };
+
+ // Convert values to int.
const value =
typeof rawValue === 'string' ? parseInt( rawValue, 10 ) : rawValue;
diff --git a/plugins/woocommerce-blocks/packages/components/formatted-monetary-amount/test/index.js b/plugins/woocommerce-blocks/packages/components/formatted-monetary-amount/test/index.js
index eedfab44185..f6024aaae3c 100644
--- a/plugins/woocommerce-blocks/packages/components/formatted-monetary-amount/test/index.js
+++ b/plugins/woocommerce-blocks/packages/components/formatted-monetary-amount/test/index.js
@@ -8,8 +8,27 @@ import { render, screen } from '@testing-library/react';
*/
import FormattedMonetaryAmount from '../index';
+jest.mock( '@woocommerce/settings', () => ( {
+ ...jest.requireActual( '@woocommerce/settings' ),
+ SITE_CURRENCY: {
+ code: 'EUR',
+ symbol: 'TEST',
+ thousandSeparator: '.',
+ decimalSeparator: ',',
+ minorUnit: 2,
+ prefix: '',
+ suffix: ' TEST',
+ },
+} ) );
+
describe( 'FormattedMonetaryAmount', () => {
describe( 'separators', () => {
+ test( 'should default to store currency configuration', () => {
+ render( );
+
+ expect( screen.getByText( '1.563,45 TEST' ) ).toBeInTheDocument();
+ } );
+
test( 'should add the thousand separator', () => {
render(
{
code: 'EUR',
symbol: '€',
decimalSeparator: ',',
+ thousandSeparator: '',
minorUnit: 2,
prefix: '',
suffix: ' €',
@@ -76,6 +96,7 @@ describe( 'FormattedMonetaryAmount', () => {
thousandSeparator: '.',
decimalSeparator: ',',
minorUnit: 2,
+ prefix: '',
suffix: ' €',
} }
/>
@@ -94,6 +115,7 @@ describe( 'FormattedMonetaryAmount', () => {
decimalSeparator: ',',
minorUnit: 2,
prefix: '€ ',
+ suffix: '',
} }
/>
);
@@ -112,6 +134,7 @@ describe( 'FormattedMonetaryAmount', () => {
thousandSeparator: '.',
decimalSeparator: ',',
minorUnit: 0,
+ prefix: '',
suffix: ' €',
} }
/>
@@ -130,6 +153,7 @@ describe( 'FormattedMonetaryAmount', () => {
decimalSeparator: ',',
minorUnit: 0,
prefix: '€ ',
+ suffix: '',
} }
/>
);
diff --git a/plugins/woocommerce-blocks/packages/components/radio-control/style.scss b/plugins/woocommerce-blocks/packages/components/radio-control/style.scss
index 77cd9020e8a..a0ee943e785 100644
--- a/plugins/woocommerce-blocks/packages/components/radio-control/style.scss
+++ b/plugins/woocommerce-blocks/packages/components/radio-control/style.scss
@@ -161,7 +161,6 @@
background: #fff;
border: 1px solid $universal-border-medium;
border-radius: 50%;
- color: currentColor;
display: inline-block;
height: em(24px); // =1.5rem
min-height: 24px;
@@ -175,16 +174,16 @@
margin: inherit;
cursor: pointer;
&:checked {
- border: 1px solid currentColor;
+ border: 1px solid $universal-border-strong;
}
&:focus {
- outline: 2px solid currentColor;
+ outline: 1px solid $universal-border-strong;
outline-offset: 2px;
}
&:checked::before {
- background: currentColor;
+ background: #000;
border-radius: 50%;
content: "";
display: block;
diff --git a/plugins/woocommerce-blocks/packages/components/text-input/style.scss b/plugins/woocommerce-blocks/packages/components/text-input/style.scss
index 08b576acb1f..3d9ba6a8262 100644
--- a/plugins/woocommerce-blocks/packages/components/text-input/style.scss
+++ b/plugins/woocommerce-blocks/packages/components/text-input/style.scss
@@ -5,6 +5,7 @@
white-space: nowrap;
label {
+ @include reset-color();
@include reset-typography();
@include font-size(regular);
position: absolute;
@@ -13,7 +14,7 @@
left: em($gap-smaller + 1px);
top: 0;
transform-origin: top left;
- color: currentColor;
+ color: $universal-body-low-emphasis;
transition: all 200ms ease;
margin: 0;
overflow: hidden;
@@ -42,26 +43,28 @@
input[type="url"],
input[type="text"],
input[type="number"],
+ input[type="password"],
input[type="email"] {
@include font-size(regular);
padding: em($gap) em($gap-smaller);
line-height: em($gap);
width: 100%;
border-radius: $universal-border-radius;
- border: 1px solid $universal-border-light;
+ border: 1px solid $universal-border-strong;
font-family: inherit;
margin: 0;
box-sizing: border-box;
min-height: 0;
- max-height: 52px;
+ height: 50px;
background-color: #fff;
- color: currentColor;
+ color: $input-text-active;
&:focus {
background-color: #fff;
+ color: $input-text-active;
outline: 0;
- box-shadow: 0 0 0 2px currentColor;
+ box-shadow: 0 0 0 1px $input-border-gray;
}
.has-dark-controls & {
@@ -72,7 +75,7 @@
&:focus {
background-color: $input-background-dark;
color: $input-text-dark;
- box-shadow: 0 0 0 2px $input-border-dark;
+ box-shadow: 0 0 0 1px $input-border-dark;
}
}
}
@@ -82,13 +85,14 @@
&.is-active input[type="url"],
&.is-active input[type="text"],
&.is-active input[type="number"],
+ &.is-active input[type="password"],
&.is-active input[type="email"] {
padding: em($gap + $gap-smaller) em($gap-smaller) em($gap-smaller);
}
&.is-active label,
input:-webkit-autofill + label {
- transform: translateY(25%) scale(0.75);
+ transform: translateY(#{$gap-smaller}) scale(0.75);
}
&.has-error input {
diff --git a/plugins/woocommerce-blocks/packages/components/text-input/types.ts b/plugins/woocommerce-blocks/packages/components/text-input/types.ts
index 18436041594..cfea0a0923b 100644
--- a/plugins/woocommerce-blocks/packages/components/text-input/types.ts
+++ b/plugins/woocommerce-blocks/packages/components/text-input/types.ts
@@ -20,6 +20,8 @@ export interface ValidatedTextInputProps
ariaDescribedBy?: string | undefined;
// id to use for the error message. If not provided, an id will be generated.
errorId?: string;
+ // Feedback to display alongside the input. May be hidden when validation errors are displayed.
+ feedback?: JSX.Element | null;
// if true, the input will be focused on mount.
focusOnMount?: boolean;
// Callback to run on change which is passed the updated value.
@@ -36,6 +38,8 @@ export interface ValidatedTextInputProps
customValidation?:
| ( ( inputObject: HTMLInputElement ) => boolean )
| undefined;
+ // Custom validation message to display when validity is false. Given the input element. Expected to use inputObject.validity.
+ customValidityMessage?: ( validity: ValidityState ) => undefined | string;
// Custom formatted to format values as they are typed.
customFormatter?: ( value: string ) => string;
// Whether validation should run when focused - only has an effect when focusOnMount is also true.
diff --git a/plugins/woocommerce-blocks/packages/components/text-input/validated-text-input.tsx b/plugins/woocommerce-blocks/packages/components/text-input/validated-text-input.tsx
index 54eeb7e65db..583690fa340 100644
--- a/plugins/woocommerce-blocks/packages/components/text-input/validated-text-input.tsx
+++ b/plugins/woocommerce-blocks/packages/components/text-input/validated-text-input.tsx
@@ -49,6 +49,8 @@ const ValidatedTextInput = forwardRef<
errorMessage: passedErrorMessage = '',
value = '',
customValidation = () => true,
+ customValidityMessage,
+ feedback = null,
customFormatter = ( newValue: string ) => newValue,
label,
validateOnMount = true,
@@ -122,14 +124,22 @@ const ValidatedTextInput = forwardRef<
setValidationErrors( {
[ errorIdString ]: {
- message: label
- ? getValidityMessageForInput( label, inputObject )
- : inputObject.validationMessage,
+ message: getValidityMessageForInput(
+ label,
+ inputObject,
+ customValidityMessage
+ ),
hidden: errorsHidden,
},
} );
},
- [ clearValidationError, errorIdString, setValidationErrors, label ]
+ [
+ clearValidationError,
+ errorIdString,
+ setValidationErrors,
+ label,
+ customValidityMessage,
+ ]
);
// Allows parent to trigger revalidation.
@@ -232,12 +242,14 @@ const ValidatedTextInput = forwardRef<
id={ textInputId }
type={ type }
feedback={
- showError ? (
+ showError && hasError ? (
- ) : null
+ ) : (
+ feedback
+ )
}
ref={ inputRef }
onChange={ ( newValue ) => {
diff --git a/plugins/woocommerce-blocks/packages/components/textarea/style.scss b/plugins/woocommerce-blocks/packages/components/textarea/style.scss
index 091fda02d5a..efd2368774c 100644
--- a/plugins/woocommerce-blocks/packages/components/textarea/style.scss
+++ b/plugins/woocommerce-blocks/packages/components/textarea/style.scss
@@ -3,7 +3,7 @@
background-color: #fff;
border: 1px solid $universal-border-strong;
border-radius: $universal-border-radius;
- color: currentColor;
+ color: $input-text-active;
font-family: inherit;
line-height: 1.375; // =22px when font-size is 16px.
margin: 0;
diff --git a/plugins/woocommerce-blocks/packages/prices/utils/price.ts b/plugins/woocommerce-blocks/packages/prices/utils/price.ts
index 54a7779d744..ab47e1a24ad 100644
--- a/plugins/woocommerce-blocks/packages/prices/utils/price.ts
+++ b/plugins/woocommerce-blocks/packages/prices/utils/price.ts
@@ -1,69 +1,13 @@
/**
* External dependencies
*/
-import { CURRENCY } from '@woocommerce/settings';
+import { SITE_CURRENCY } from '@woocommerce/settings';
import type {
Currency,
CurrencyResponse,
CartShippingPackageShippingRate,
- SymbolPosition,
} from '@woocommerce/types';
-/**
- * Get currency prefix.
- */
-const getPrefix = (
- // Currency symbol.
- symbol: string,
- // Position of currency symbol from settings.
- symbolPosition: SymbolPosition
-): string => {
- const prefixes = {
- left: symbol,
- left_space: ' ' + symbol,
- right: '',
- right_space: '',
- };
- return prefixes[ symbolPosition ] || '';
-};
-
-/**
- * Get currency suffix.
- */
-const getSuffix = (
- // Currency symbol.
- symbol: string,
- // Position of currency symbol from settings.
- symbolPosition: SymbolPosition
-): string => {
- const suffixes = {
- left: '',
- left_space: '',
- right: symbol,
- right_space: ' ' + symbol,
- };
- return suffixes[ symbolPosition ] || '';
-};
-
-/**
- * Currency information in normalized format from server settings.
- */
-const siteCurrencySettings: Currency = {
- code: CURRENCY.code,
- symbol: CURRENCY.symbol,
- thousandSeparator: CURRENCY.thousandSeparator,
- decimalSeparator: CURRENCY.decimalSeparator,
- minorUnit: CURRENCY.precision,
- prefix: getPrefix(
- CURRENCY.symbol,
- CURRENCY.symbolPosition as SymbolPosition
- ),
- suffix: getSuffix(
- CURRENCY.symbol,
- CURRENCY.symbolPosition as SymbolPosition
- ),
-};
-
/**
* Gets currency information in normalized format from an API response or the server.
*
@@ -77,7 +21,7 @@ export const getCurrencyFromPriceResponse = (
| CartShippingPackageShippingRate
): Currency => {
if ( ! currencyData?.currency_code ) {
- return siteCurrencySettings;
+ return SITE_CURRENCY;
}
const {
@@ -91,15 +35,21 @@ export const getCurrencyFromPriceResponse = (
} = currencyData;
return {
- code: code || 'USD',
- symbol: symbol || '$',
+ code: code || SITE_CURRENCY.code,
+ symbol: symbol || SITE_CURRENCY.symbol,
thousandSeparator:
- typeof thousandSeparator === 'string' ? thousandSeparator : ',',
+ typeof thousandSeparator === 'string'
+ ? thousandSeparator
+ : SITE_CURRENCY.thousandSeparator,
decimalSeparator:
- typeof decimalSeparator === 'string' ? decimalSeparator : '.',
- minorUnit: Number.isFinite( minorUnit ) ? minorUnit : 2,
- prefix: typeof prefix === 'string' ? prefix : '$',
- suffix: typeof suffix === 'string' ? suffix : '',
+ typeof decimalSeparator === 'string'
+ ? decimalSeparator
+ : SITE_CURRENCY.decimalSeparator,
+ minorUnit: Number.isFinite( minorUnit )
+ ? minorUnit
+ : SITE_CURRENCY.minorUnit,
+ prefix: typeof prefix === 'string' ? prefix : SITE_CURRENCY.prefix,
+ suffix: typeof suffix === 'string' ? suffix : SITE_CURRENCY.suffix,
};
};
@@ -110,7 +60,7 @@ export const getCurrency = (
currencyData: Partial< Currency > = {}
): Currency => {
return {
- ...siteCurrencySettings,
+ ...SITE_CURRENCY,
...currencyData,
};
};
diff --git a/plugins/woocommerce-blocks/patches/wordpress-components+14.2.0.patch b/plugins/woocommerce-blocks/patches/wordpress-components+14.2.0.patch
deleted file mode 100644
index 69a0ce6e4f5..00000000000
--- a/plugins/woocommerce-blocks/patches/wordpress-components+14.2.0.patch
+++ /dev/null
@@ -1,44 +0,0 @@
-diff --git a/node_modules/wordpress-components/build-module/combobox-control/index.js b/node_modules/wordpress-components/build-module/combobox-control/index.js
-index ddef775..2d0b3ab 100644
---- a/node_modules/wordpress-components/build-module/combobox-control/index.js
-+++ b/node_modules/wordpress-components/build-module/combobox-control/index.js
-@@ -55,6 +55,7 @@ function ComboboxControl({
- const instanceId = useInstanceId(ComboboxControl);
- const [selectedSuggestion, setSelectedSuggestion] = useState(null);
- const [isExpanded, setIsExpanded] = useState(false);
-+ const [inputHasFocus, setInputHasFocus] = useState( false );
- const [inputValue, setInputValue] = useState('');
- const inputContainer = useRef();
- const currentOption = options.find(option => option.value === value);
-@@ -135,7 +136,12 @@ function ComboboxControl({
- }
- };
-
-+ const onBlur = () => {
-+ setInputHasFocus( false );
-+ };
-+
- const onFocus = () => {
-+ setInputHasFocus( true );
- setIsExpanded(true);
- onFilterValueChange('');
- setInputValue('');
-@@ -149,7 +155,9 @@ function ComboboxControl({
- const text = event.value;
- setInputValue(text);
- onFilterValueChange(text);
-- setIsExpanded(true);
-+ if ( inputHasFocus ) {
-+ setIsExpanded( true );
-+ }
- };
-
- const handleOnReset = () => {
-@@ -193,6 +201,7 @@ function ComboboxControl({
- value: isExpanded ? inputValue : currentLabel,
- "aria-label": currentLabel ? `${currentLabel}, ${label}` : null,
- onFocus: onFocus,
-+ onBlur: onBlur,
- isExpanded: isExpanded,
- selectedSuggestionIndex: matchingSuggestions.indexOf(selectedSuggestion),
- onChange: onInputChange
diff --git a/plugins/woocommerce-blocks/tests/e2e/bin/test-env-setup.sh b/plugins/woocommerce-blocks/tests/e2e/bin/test-env-setup.sh
index 253186a296d..b80fbed4649 100755
--- a/plugins/woocommerce-blocks/tests/e2e/bin/test-env-setup.sh
+++ b/plugins/woocommerce-blocks/tests/e2e/bin/test-env-setup.sh
@@ -1,13 +1,7 @@
#!/usr/bin/env bash
-# Extract the relative path from the plugin root to this script directory.
-# By doing so, we can run this script from anywhere.
-script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-head_dir=$(cd "$(dirname "$script_dir")" && cd ../../.. && pwd)
-relative_path=${script_dir#$head_dir/}
-
# Remove the database snapshot if it exists.
-wp-env run tests-cli -- rm blocks_e2e.sql 2> /dev/null
+wp-env run tests-cli -- rm -f blocks_e2e.sql
# Run the main script in the container for better performance.
wp-env run tests-cli -- bash wp-content/plugins/woocommerce/blocks-bin/playwright/scripts/index.sh
# Disable the LYS Coming Soon banner.
diff --git a/plugins/woocommerce-blocks/tests/e2e/playwright.config.ts b/plugins/woocommerce-blocks/tests/e2e/playwright.config.ts
index 44b30868bed..f1ad703ebf1 100644
--- a/plugins/woocommerce-blocks/tests/e2e/playwright.config.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/playwright.config.ts
@@ -8,20 +8,18 @@ import { PlaywrightTestConfig, defineConfig, devices } from '@playwright/test';
const { CI, DEFAULT_TIMEOUT_OVERRIDE } = process.env;
const config: PlaywrightTestConfig = {
- maxFailures: 0,
+ maxFailures: CI ? 30 : 0,
timeout: parseInt( DEFAULT_TIMEOUT_OVERRIDE || '', 10 ) || 100_000, // Defaults to 100s.
outputDir: `${ __dirname }/artifacts/test-results`,
globalSetup: fileURLToPath(
new URL( 'global-setup.ts', 'file:' + __filename ).href
),
testDir: './tests',
- retries: CI ? 2 : 0,
- // We're running our tests in serial, so we only need one worker.
+ retries: CI ? 1 : 0,
workers: 1,
+ reportSlowTests: { max: 5, threshold: 30 * 1000 }, // 30 seconds threshold
fullyParallel: false,
forbidOnly: !! CI,
- // Don't report slow test "files", as we're running our tests in serial.
- reportSlowTests: null,
reporter: process.env.CI
? [
[ 'github' ],
@@ -38,7 +36,10 @@ const config: PlaywrightTestConfig = {
use: {
baseURL: BASE_URL,
screenshot: 'only-on-failure',
- trace: 'retain-on-failure',
+ trace:
+ /^https?:\/\/localhost/.test( BASE_URL ) || ! CI
+ ? 'retain-on-first-failure'
+ : 'off',
video: 'on-first-retry',
viewport: { width: 1280, height: 720 },
storageState: STORAGE_STATE_PATH,
diff --git a/plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection-tester/index.js b/plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection-tester/index.js
index 931a6a0266e..af8015a43b6 100644
--- a/plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection-tester/index.js
+++ b/plugins/woocommerce-blocks/tests/e2e/plugins/register-product-collection-tester/index.js
@@ -11,6 +11,7 @@ __experimentalRegisterProductCollection( {
query: {
perPage: 5,
},
+ hideControls: [ 'keyword', 'on-sale' ],
},
} );
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/additional-fields.guest-shopper.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/additional-fields.guest-shopper.block_theme.spec.ts
index 7a4d9042897..a43ff26ec11 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/additional-fields.guest-shopper.block_theme.spec.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/additional-fields.guest-shopper.block_theme.spec.ts
@@ -76,11 +76,6 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
'Please enter a valid government id'
)
).toBeVisible();
- await expect(
- checkoutPageObject.page.getByText(
- 'Please select a valid option'
- )
- ).toBeVisible();
} );
test( 'Shopper can fill in the checkout form with additional fields and can have different value for same field in shipping and billing address', async ( {
@@ -101,39 +96,21 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
shipping: {
'Government ID': '12345',
'Confirm government ID': '12345',
+ 'How wide is your road? (optional)': 'wide',
},
billing: {
'Government ID': '54321',
'Confirm government ID': '54321',
+ 'How wide is your road? (optional)': 'narrow',
},
},
order: {
- 'How did you hear about us?': 'Other',
+ 'How did you hear about us? (optional)': 'other',
'What is your favourite colour?': 'Blue',
},
}
);
- // Fill select fields "manually" (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
- // fields until we recreate th Combobox component. This is because the aria-label includes the value so getting
- // by label alone is not reliable unless we know the value.
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'wide' );
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'narrow' );
-
- await checkoutPageObject.page.evaluate(
- 'document.activeElement.blur()'
- );
-
await checkoutPageObject.page
.getByLabel( 'Would you like a free gift with your order?' )
.check();
@@ -217,7 +194,7 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
.getByLabel(
'Is this a personal purchase or a business purchase?'
)
- ).toHaveValue( 'Business' );
+ ).toHaveValue( 'business' );
await expect(
checkoutPageObject.page
.getByRole( 'group', {
@@ -251,8 +228,8 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
.getByRole( 'group', {
name: 'Shipping address',
} )
- .getByLabel( 'How wide is your road?' )
- ).toHaveValue( 'Wide' );
+ .getByLabel( 'How wide is your road? (optional)' )
+ ).toHaveValue( 'wide' );
await expect(
checkoutPageObject.page
.getByRole( 'group', {
@@ -279,8 +256,8 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
.getByRole( 'group', {
name: 'Billing address',
} )
- .getByLabel( 'How wide is your road?' )
- ).toHaveValue( 'Narrow' );
+ .getByLabel( 'How wide is your road? (optional)' )
+ ).toHaveValue( 'narrow' );
} );
} );
} );
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/additional-fields.merchant.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/additional-fields.merchant.block_theme.spec.ts
index bdc9b0e03df..6b82919576b 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/additional-fields.merchant.block_theme.spec.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/additional-fields.merchant.block_theme.spec.ts
@@ -70,13 +70,13 @@ test.describe( 'Merchant → Additional Checkout Fields', () => {
name: 'Shipping address',
} )
.getByLabel( 'How wide is your road?' )
- .fill( 'wide' );
+ .selectOption( 'wide' );
await checkoutPageObject.page
.getByRole( 'group', {
name: 'Billing address',
} )
.getByLabel( 'How wide is your road?' )
- .fill( 'narrow' );
+ .selectOption( 'narrow' );
await checkoutPageObject.page.evaluate(
'document.activeElement.blur()'
@@ -206,13 +206,13 @@ test.describe( 'Merchant → Additional Checkout Fields', () => {
name: 'Shipping address',
} )
.getByLabel( 'How wide is your road?' )
- .fill( 'wide' );
+ .selectOption( 'wide' );
await checkoutPageObject.page
.getByRole( 'group', {
name: 'Billing address',
} )
.getByLabel( 'How wide is your road?' )
- .fill( 'narrow' );
+ .selectOption( 'narrow' );
await checkoutPageObject.page.evaluate(
'document.activeElement.blur()'
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/additional-fields.shopper.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/additional-fields.shopper.block_theme.spec.ts
index 6e377fe0965..51604a636e7 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/additional-fields.shopper.block_theme.spec.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/additional-fields.shopper.block_theme.spec.ts
@@ -52,10 +52,12 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
shipping: {
'Government ID': '12345',
'Confirm government ID': '12345',
+ 'How wide is your road? (optional)': 'wide',
},
billing: {
'Government ID': '54321',
'Confirm government ID': '54321',
+ 'How wide is your road? (optional)': 'narrow',
},
},
order: {
@@ -65,26 +67,6 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
}
);
- // Fill select fields "manually" (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
- // fields until we recreate th Combobox component. This is because the aria-label includes the value so getting
- // by label alone is not reliable unless we know the value.
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'wide' );
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'narrow' );
-
- await checkoutPageObject.page.evaluate(
- 'document.activeElement.blur()'
- );
-
await checkoutPageObject.page
.getByLabel( 'Would you like a free gift with your order?' )
.check();
@@ -155,7 +137,7 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
.getByLabel(
'Is this a personal purchase or a business purchase?'
)
- ).toHaveValue( 'Business' );
+ ).toHaveValue( 'business' );
await expect(
checkoutPageObject.page
.getByRole( 'group', {
@@ -189,8 +171,8 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
.getByRole( 'group', {
name: 'Shipping address',
} )
- .getByLabel( 'How wide is your road?' )
- ).toHaveValue( 'Wide' );
+ .getByLabel( 'How wide is your road? (optional)' )
+ ).toHaveValue( 'wide' );
await expect(
checkoutPageObject.page
.getByRole( 'group', {
@@ -217,8 +199,8 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
.getByRole( 'group', {
name: 'Billing address',
} )
- .getByLabel( 'How wide is your road?' )
- ).toHaveValue( 'Narrow' );
+ .getByLabel( 'How wide is your road? (optional)' )
+ ).toHaveValue( 'narrow' );
} );
test( 'Shopper can change the values of fields multiple times and place the order', async ( {
@@ -240,10 +222,12 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
shipping: {
'Government ID': '12345',
'Confirm government ID': '12345',
+ 'How wide is your road? (optional)': 'wide',
},
billing: {
'Government ID': '54321',
'Confirm government ID': '54321',
+ 'How wide is your road? (optional)': 'narrow',
},
},
order: {
@@ -253,36 +237,23 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
}
);
- // Fill select fields "manually" (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
- // fields until we recreate the Combobox component. This is because the aria-label includes the value so getting
- // by label alone is not reliable unless we know the value.
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'wide' );
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'narrow' );
await checkoutPageObject.waitForCustomerDataUpdate();
// Change the shipping and billing select fields again.
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'wide' );
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'super-wide' );
+ await checkoutPageObject.fillInCheckoutWithTestData(
+ {},
+ {
+ address: {
+ shipping: {
+ 'How wide is your road? (optional)': 'super-wide',
+ },
+ billing: {
+ 'How wide is your road? (optional)': 'wide',
+ },
+ },
+ }
+ );
+
await checkoutPageObject.waitForCustomerDataUpdate();
await checkoutPageObject.page
@@ -341,24 +312,12 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
},
order: {
'What is your favourite colour?': 'Red',
+ 'How did you hear about us?':
+ 'Select a how did you hear about us? (optional)',
},
}
);
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Additional order information',
- } )
- .getByLabel( 'How did you hear about us?' )
- .click();
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Additional order information',
- } )
- .locator(
- 'ul.components-form-token-field__suggestions-list > li'
- )
- .first()
- .click();
+
await checkoutPageObject.waitForCustomerDataUpdate();
await checkoutPageObject.placeOrder();
@@ -424,10 +383,12 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
shipping: {
'Government ID': ' 1. 2 3 4 5 ',
'Confirm government ID': '1 2345',
+ 'How wide is your road? (optional)': 'wide',
},
billing: {
'Government ID': ' 5. 4 3 2 1 ',
'Confirm government ID': '543 21',
+ 'How wide is your road? (optional)': 'narrow',
},
},
order: {
@@ -437,26 +398,6 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
}
);
- // Fill select fields "manually" (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
- // fields until we recreate th Combobox component. This is because the aria-label includes the value so getting
- // by label alone is not reliable unless we know the value.
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'wide' );
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'narrow' );
-
- await checkoutPageObject.page.evaluate(
- 'document.activeElement.blur()'
- );
-
await checkoutPageObject.page
.getByLabel( 'Would you like a free gift with your order?' )
.check();
@@ -527,7 +468,7 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
.getByLabel(
'Is this a personal purchase or a business purchase?'
)
- ).toHaveValue( 'Business' );
+ ).toHaveValue( 'business' );
await expect(
checkoutPageObject.page
.getByRole( 'group', {
@@ -561,8 +502,8 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
.getByRole( 'group', {
name: 'Shipping address',
} )
- .getByLabel( 'How wide is your road?' )
- ).toHaveValue( 'Wide' );
+ .getByLabel( 'How wide is your road? (optional)' )
+ ).toHaveValue( 'wide' );
await expect(
checkoutPageObject.page
.getByRole( 'group', {
@@ -589,8 +530,8 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
.getByRole( 'group', {
name: 'Billing address',
} )
- .getByLabel( 'How wide is your road?' )
- ).toHaveValue( 'Narrow' );
+ .getByLabel( 'How wide is your road? (optional)' )
+ ).toHaveValue( 'narrow' );
} );
test( 'Shopper can see server-side validation errors', async ( {
@@ -664,35 +605,18 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
shipping: {
'Government ID': '12345',
'Confirm government ID': '12345',
+ 'How wide is your road? (optional)': 'wide',
},
billing: {
'Government ID': '54321',
'Confirm government ID': '54321',
+ 'How wide is your road? (optional)': 'narrow',
},
},
order: { 'How did you hear about us?': 'Other' },
}
);
- // Fill select fields "manually" (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
- // fields until we recreate th Combobox component. This is because the aria-label includes the value so getting
- // by label alone is not reliable unless we know the value.
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'wide' );
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'narrow' );
- await checkoutPageObject.page.evaluate(
- 'document.activeElement.blur()'
- );
-
await checkoutPageObject.placeOrder();
expect(
@@ -725,10 +649,12 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
shipping: {
'Government ID': '12345',
'Confirm government ID': '12345',
+ 'How wide is your road? (optional)': 'wide',
},
billing: {
'Government ID': '54321',
'Confirm government ID': '54321',
+ 'How wide is your road? (optional)': 'narrow',
},
},
order: { 'How did you hear about us?': 'Other' },
@@ -758,26 +684,6 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
.getByLabel( 'Can a truck fit down your road?' )
.uncheck();
- // Fill select fields manually. (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
- // fields until we recreate th Combobox component. This is because the aria-label includes the value so getting
- // by label alone is not reliable unless we know the value.
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Shipping address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'wide' );
- await checkoutPageObject.page
- .getByRole( 'group', {
- name: 'Billing address',
- } )
- .getByLabel( 'How wide is your road?' )
- .fill( 'narrow' );
-
- // Blur after editing the select fields since they need to be blurred to save.
- await checkoutPageObject.page.evaluate(
- 'document.activeElement.blur()'
- );
await checkoutPageObject.placeOrder();
expect(
@@ -855,7 +761,7 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
// Check select in edit mode match the expected value.
const roadSizeSelect = checkoutPageObject.page.getByLabel(
- 'How wide is your road?'
+ 'How wide is your road? (optional)'
);
await expect( roadSizeSelect ).toHaveValue( 'narrow' );
@@ -918,14 +824,14 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
// Check select in edit mode match the expected value.
const shippingRoadSizeSelect = checkoutPageObject.page.getByLabel(
- 'How wide is your road?'
+ 'How wide is your road? (optional)'
);
await expect( shippingRoadSizeSelect ).toHaveValue( 'wide' );
await govIdInput.fill( '11111' );
await confirmGovIdInput.fill( '11111' );
await shippingTruckFittingCheckbox.uncheck();
- await shippingRoadSizeSelect.selectOption( 'Narrow' );
+ await shippingRoadSizeSelect.selectOption( 'narrow' );
await checkoutPageObject.page.getByText( 'Save address' ).click();
// Check the updated values are visible in the addresses.
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout-block.merchant.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout-block.merchant.block_theme.spec.ts
index 02308e2307f..d953507dc2c 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout-block.merchant.block_theme.spec.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout-block.merchant.block_theme.spec.ts
@@ -487,7 +487,7 @@ test.describe( 'Merchant → Checkout', () => {
await expect( shippingApartmentLink ).toBeVisible();
// Verify that the apartment field is hidden by default and the field is optional.
- await expect( shippingApartmentInput ).toBeHidden();
+ await expect( shippingApartmentInput ).not.toBeInViewport();
await expect( shippingApartmentOptionalToggle ).toBeChecked();
// Make the apartment number required.
@@ -504,7 +504,7 @@ test.describe( 'Merchant → Checkout', () => {
// Verify that the apartment link and the apartment field are hidden.
await expect( shippingApartmentLink ).toBeHidden();
- await expect( shippingApartmentInput ).toBeHidden();
+ await expect( shippingApartmentInput ).not.toBeInViewport();
// Display the billing address form.
await editor.canvas
@@ -568,7 +568,7 @@ test.describe( 'Merchant → Checkout', () => {
await expect( billingApartmentLink ).toBeVisible();
// Verify that the apartment field is hidden and optional.
- await expect( billingApartmentInput ).toBeHidden();
+ await expect( billingApartmentInput ).not.toBeInViewport();
await expect( billingApartmentOptionalToggle ).toBeChecked();
// Disable the apartment field.
@@ -576,7 +576,7 @@ test.describe( 'Merchant → Checkout', () => {
// Verify that the apartment link and the apartment field are hidden.
await expect( billingApartmentLink ).toBeHidden();
- await expect( billingApartmentInput ).toBeHidden();
+ await expect( billingApartmentInput ).not.toBeInViewport();
} );
test( 'Phone input visibility and optional and required can be toggled', async ( {
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout-block.shopper.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout-block.shopper.block_theme.spec.ts
index a280ca67491..de86eff5685 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout-block.shopper.block_theme.spec.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout-block.shopper.block_theme.spec.ts
@@ -71,7 +71,7 @@ test.describe( 'Shopper → Account (guest user)', () => {
baseURL,
} ) => {
//Get the login link from checkout page.
- const loginLink = page.getByRole( 'link', { name: 'Log in.' } );
+ const loginLink = page.getByRole( 'link', { name: 'Log in' } );
await expect( loginLink ).toHaveAttribute(
'href',
@@ -85,10 +85,15 @@ test.describe( 'Shopper → Account (guest user)', () => {
path: 'wc/v3/settings/account/woocommerce_enable_signup_and_login_from_checkout',
data: { value: 'yes' },
} );
+ await requestUtils.rest( {
+ method: 'PUT',
+ path: 'wc/v3/settings/account/woocommerce_registration_generate_password',
+ data: { value: 'yes' },
+ } );
await page.reload();
- const createAccount = page.getByLabel( 'Create an account?' );
+ const createAccount = page.getByLabel( 'Create an account' );
await createAccount.check();
const testEmail = `test-${ Date.now() }@example.com`;
@@ -228,6 +233,7 @@ test.describe( 'Shopper → Shipping and Billing Addresses', () => {
city: 'San Francisco',
state: 'California',
country: 'United Kingdom',
+ countryKey: 'GB',
postcode: 'SW1 1AA',
phone: '123456789',
email: 'john.doe@example.com',
@@ -241,6 +247,7 @@ test.describe( 'Shopper → Shipping and Billing Addresses', () => {
city: 'Los Angeles',
phone: '987654321',
country: 'Albania',
+ countryKey: 'AL',
state: 'Berat',
postcode: '1234',
};
@@ -349,7 +356,7 @@ test.describe( 'Shopper → Shipping (customer user)', () => {
lastname: 'Perez',
addressfirstline: '123 Test Street',
addresssecondline: 'Apartment 6',
- country: 'ES',
+ countryKey: 'ES',
city: 'Madrid',
postcode: '08830',
state: 'M',
@@ -546,6 +553,7 @@ test.describe( 'Billing Address Form', () => {
addressfirstline: '123 Easy Street',
addresssecondline: 'Testville',
country: 'United States (US)',
+ countryKey: 'US',
city: 'New York',
state: 'New York',
postcode: '90210',
@@ -571,14 +579,14 @@ test.describe( 'Billing Address Form', () => {
shippingForm.getByLabel( 'Apartment, suite, etc. (optional)' )
).toHaveValue( 'Testville' );
await expect(
- shippingForm.getByLabel( 'United States (US), Country/' )
- ).toHaveValue( 'United States (US)' );
+ shippingForm.getByLabel( 'Country/Region' )
+ ).toHaveValue( 'US' );
await expect( shippingForm.getByLabel( 'City' ) ).toHaveValue(
'New York'
);
- await expect(
- shippingForm.getByLabel( 'New York, State' )
- ).toHaveValue( 'New York' );
+ await expect( shippingForm.getByLabel( 'State' ) ).toHaveValue(
+ 'NY'
+ );
await expect( shippingForm.getByLabel( 'ZIP Code' ) ).toHaveValue(
'90210'
);
@@ -605,12 +613,12 @@ test.describe( 'Billing Address Form', () => {
} )
).toBeVisible();
await expect(
- billingForm.getByLabel( 'United States (US), Country/' )
- ).toHaveValue( 'United States (US)' );
+ billingForm.getByLabel( 'Country/Region' )
+ ).toHaveValue( 'US' );
await expect( billingForm.getByLabel( 'City' ) ).toHaveValue( '' );
- await expect(
- billingForm.getByLabel( 'New York, State' )
- ).toHaveValue( 'New York' );
+ await expect( billingForm.getByLabel( 'State' ) ).toHaveValue(
+ 'NY'
+ );
await expect( billingForm.getByLabel( 'ZIP Code' ) ).toHaveValue(
''
);
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout.page.ts b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout.page.ts
index 41960434059..d56d8715dc6 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout.page.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/checkout/checkout.page.ts
@@ -23,6 +23,7 @@ export class CheckoutPage {
addressfirstline: '123 Easy Street',
addresssecondline: 'Testville',
country: 'United States (US)',
+ countryKey: 'US',
city: 'New York',
state: 'New York',
postcode: '90210',
@@ -124,7 +125,16 @@ export class CheckoutPage {
// Rest of additional data passed in from the overrideData object.
for ( const [ label, value ] of Object.entries( additionalFields ) ) {
const field = contactSection.getByLabel( label );
- await field.fill( value );
+
+ const tagName = await field.evaluate( ( element ) =>
+ element.tagName.toLowerCase()
+ );
+
+ if ( tagName === 'select' ) {
+ await field.selectOption( value );
+ } else {
+ await field.fill( value );
+ }
}
}
@@ -138,7 +148,16 @@ export class CheckoutPage {
// Rest of additional data passed in from the overrideData object.
for ( const [ label, value ] of Object.entries( additionalFields ) ) {
const field = contactSection.getByLabel( label );
- await field.fill( value );
+
+ const tagName = await field.evaluate( ( element ) =>
+ element.tagName.toLowerCase()
+ );
+
+ if ( tagName === 'select' ) {
+ await field.selectOption( value );
+ } else {
+ await field.fill( value );
+ }
}
}
@@ -151,7 +170,16 @@ export class CheckoutPage {
// Rest of additional data passed in from the overrideData object.
for ( const { label, value } of additionalFields ) {
const field = this.page.getByLabel( label );
- await field.fill( value );
+
+ const tagName = await field.evaluate( ( element ) =>
+ element.tagName.toLowerCase()
+ );
+
+ if ( tagName === 'select' ) {
+ await field.selectOption( value );
+ } else {
+ await field.fill( value );
+ }
}
}
@@ -337,7 +365,7 @@ export class CheckoutPage {
await email.fill( customerBillingDetails.email );
await firstName.fill( customerBillingDetails.firstname );
await lastName.fill( customerBillingDetails.lastname );
- await country.fill( customerBillingDetails.country );
+ await country.selectOption( customerBillingDetails.countryKey );
await address1.fill( customerBillingDetails.addressfirstline );
if ( customerBillingDetails.addresssecondline ) {
@@ -360,10 +388,17 @@ export class CheckoutPage {
} );
const county = billingForm.getByLabel( 'County' );
- await state
- .or( province )
- .or( county )
- .fill( customerBillingDetails.state );
+ const elementToFill = state.or( province ).or( county );
+ const tagName = await elementToFill.evaluate( ( element ) =>
+ element.tagName.toLowerCase()
+ );
+ if ( tagName === 'select' ) {
+ await elementToFill.selectOption(
+ customerBillingDetails.state
+ );
+ } else {
+ await elementToFill.fill( customerBillingDetails.state );
+ }
}
if ( customerBillingDetails.postcode ) {
@@ -376,7 +411,16 @@ export class CheckoutPage {
// Rest of additional data passed in from the overrideData object.
for ( const [ label, value ] of Object.entries( additionalFields ) ) {
const field = billingForm.getByLabel( label, { exact: true } );
- await field.fill( value );
+
+ const tagName = await field.evaluate( ( element ) =>
+ element.tagName.toLowerCase()
+ );
+
+ if ( tagName === 'select' ) {
+ await field.selectOption( value );
+ } else {
+ await field.fill( value );
+ }
}
}
@@ -407,7 +451,7 @@ export class CheckoutPage {
await firstName.fill( customerShippingDetails.firstname );
await lastName.fill( customerShippingDetails.lastname );
- await country.fill( customerShippingDetails.country );
+ await country.selectOption( customerShippingDetails.country );
await address1.fill( customerShippingDetails.addressfirstline );
if ( customerShippingDetails.addresssecondline ) {
@@ -430,10 +474,17 @@ export class CheckoutPage {
} );
const county = shippingForm.getByLabel( 'County' );
- await state
- .or( province )
- .or( county )
- .fill( customerShippingDetails.state );
+ const elementToFill = state.or( province ).or( county );
+ const tagName = await elementToFill.evaluate( ( element ) =>
+ element.tagName.toLowerCase()
+ );
+ if ( tagName === 'select' ) {
+ await elementToFill.selectOption(
+ customerShippingDetails.state
+ );
+ } else {
+ await elementToFill.fill( customerShippingDetails.state );
+ }
}
if ( customerShippingDetails.postcode ) {
@@ -446,7 +497,16 @@ export class CheckoutPage {
// Rest of additional data passed in from the overrideData object.
for ( const [ label, value ] of Object.entries( additionalFields ) ) {
const field = shippingForm.getByLabel( label, { exact: true } );
- await field.fill( value );
+
+ const tagName = await field.evaluate( ( element ) =>
+ element.tagName.toLowerCase()
+ );
+
+ if ( tagName === 'select' ) {
+ await field.selectOption( value );
+ } else {
+ await field.fill( value );
+ }
}
// Blur active field to trigger customer address update.
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/featured-category/featured-category.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/featured-category/featured-category.block_theme.spec.ts
index 8fd407d67b8..48c2273b742 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/featured-category/featured-category.block_theme.spec.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/featured-category/featured-category.block_theme.spec.ts
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { expect, test } from '@woocommerce/e2e-utils';
+import { test, expect, wpCLI } from '@woocommerce/e2e-utils';
const blockData = {
slug: 'woocommerce/featured-category',
@@ -28,4 +28,39 @@ test.describe( `${ blockData.slug } Block`, () => {
blockLocatorFrontend.getByText( 'Shop now' )
).toBeVisible();
} );
+
+ test( 'image can be edited', async ( { editor, admin } ) => {
+ await test.step( 'Create a product category with an image', async () => {
+ // Get the id of the image associated to the Cap product (for example).
+ const productCliOutput = await wpCLI(
+ `post list --post_type=product --title=Cap --field=ID`
+ );
+ const productId = productCliOutput.stdout.match( /\d+/g )?.pop();
+ const mediaCliOutput = await wpCLI(
+ `post meta get ${ productId } _thumbnail_id`
+ );
+ const mediaId = mediaCliOutput.stdout.match( /\d+/g )?.pop();
+
+ // Create a product category with that image.
+ const categoryCliOutput = await wpCLI(
+ `wc product_cat create --name="Test Category" --slug="test-category" --image='{ "id": ${ mediaId } }' --user=1`
+ );
+ const categoryId = categoryCliOutput.stdout.match( /\d+/g )?.pop();
+ await wpCLI(
+ `wc product update ${ productId } --categories='[ { "id": ${ categoryId } } ]' --user=1`
+ );
+ } );
+
+ await admin.createNewPost();
+ await editor.insertBlock( { name: blockData.slug } );
+ const blockLocator = await editor.getBlockByName( blockData.slug );
+ await blockLocator.getByText( 'Test Category' ).click();
+ await blockLocator.getByText( 'Done' ).click();
+ await editor.page.getByLabel( 'Edit category image' ).click();
+ await editor.page.getByLabel( 'Rotate' ).click();
+ await editor.page.getByRole( 'button', { name: 'Apply' } ).click();
+ await expect(
+ editor.page.locator( 'img[alt="Test Category"][src*="-edited"]' )
+ ).toBeVisible();
+ } );
} );
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/featured-product/featured-product.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/featured-product/featured-product.block_theme.spec.ts
index 94657793414..54056ab2485 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/featured-product/featured-product.block_theme.spec.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/featured-product/featured-product.block_theme.spec.ts
@@ -28,4 +28,18 @@ test.describe( `${ blockData.slug } Block`, () => {
blockLocatorFrontend.getByText( 'Shop now' )
).toBeVisible();
} );
+
+ test( 'image can be edited', async ( { editor, admin } ) => {
+ await admin.createNewPost();
+ await editor.insertBlock( { name: blockData.slug } );
+ const blockLocator = await editor.getBlockByName( blockData.slug );
+ await blockLocator.getByText( 'Album' ).click();
+ await blockLocator.getByText( 'Done' ).click();
+ await editor.page.getByLabel( 'Edit product image' ).click();
+ await editor.page.getByLabel( 'Rotate' ).click();
+ await editor.page.getByRole( 'button', { name: 'Apply' } ).click();
+ await expect(
+ editor.page.locator( 'img[alt="Album"][src*="-edited"]' )
+ ).toBeVisible();
+ } );
} );
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts
index 49d3cceb71b..fabb438e92b 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.block_theme.spec.ts
@@ -28,7 +28,6 @@ test.describe( 'Product Collection', () => {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
- expect( pageObject.productTemplate ).not.toBeNull();
await expect( pageObject.products ).toHaveCount( 9 );
await expect( pageObject.productImages ).toHaveCount( 9 );
await expect( pageObject.productTitles ).toHaveCount( 9 );
@@ -37,7 +36,6 @@ test.describe( 'Product Collection', () => {
await pageObject.publishAndGoToFrontend();
- expect( pageObject.productTemplate ).not.toBeNull();
await expect( pageObject.products ).toHaveCount( 9 );
await expect( pageObject.productImages ).toHaveCount( 9 );
await expect( pageObject.productTitles ).toHaveCount( 9 );
@@ -46,44 +44,6 @@ test.describe( 'Product Collection', () => {
} );
test.describe( 'Renders correctly with all Product Elements', () => {
- const insertProductElements = async (
- pageObject: ProductCollectionPage
- ) => {
- // By default there are inner blocks:
- // - woocommerce/product-image
- // - core/post-title
- // - woocommerce/product-price
- // - woocommerce/product-button
- // We're adding remaining ones
- const productElements = [
- { name: 'woocommerce/product-rating', attributes: {} },
- { name: 'woocommerce/product-sku', attributes: {} },
- { name: 'woocommerce/product-stock-indicator', attributes: {} },
- { name: 'woocommerce/product-sale-badge', attributes: {} },
- {
- name: 'core/post-excerpt',
- attributes: {
- __woocommerceNamespace:
- 'woocommerce/product-collection/product-summary',
- },
- },
- {
- name: 'core/post-terms',
- attributes: { term: 'product_tag' },
- },
- {
- name: 'core/post-terms',
- attributes: { term: 'product_cat' },
- },
- ];
-
- for ( const productElement of productElements ) {
- await pageObject.insertBlockInProductCollection(
- productElement
- );
- }
- };
-
const expectedProductContent = [
'Beanie', // core/post-title
'$20.00 Original price was: $20.00.$18.00Current price is: $18.00.', // woocommerce/product-price
@@ -103,7 +63,7 @@ test.describe( 'Product Collection', () => {
page.locator( '[data-testid="product-image"]:visible' )
).toHaveCount( 9 );
- await insertProductElements( pageObject );
+ await pageObject.insertProductElements();
await pageObject.publishAndGoToFrontend();
for ( const content of expectedProductContent ) {
@@ -124,7 +84,7 @@ test.describe( 'Product Collection', () => {
editor.canvas.locator( '[data-testid="product-image"]:visible' )
).toHaveCount( 16 );
- await insertProductElements( pageObject );
+ await pageObject.insertProductElements();
await editor.saveSiteEditorEntities( {
isOnlyCurrentEntityDirty: true,
} );
@@ -152,7 +112,7 @@ test.describe( 'Product Collection', () => {
editor.canvas.locator( '[data-testid="product-image"]:visible' )
).toHaveCount( 9 );
- await insertProductElements( pageObject );
+ await pageObject.insertProductElements();
await editor.saveSiteEditorEntities( {
isOnlyCurrentEntityDirty: true,
} );
@@ -166,14 +126,12 @@ test.describe( 'Product Collection', () => {
} );
} );
- test.describe( 'Product Collection Sidebar Settings', () => {
- test.beforeEach( async ( { pageObject } ) => {
- await pageObject.createNewPostAndInsertBlock();
- } );
-
+ test.describe( 'Inspector Controls', () => {
test( 'Reflects the correct number of columns according to sidebar settings', async ( {
pageObject,
} ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
await pageObject.setNumberOfColumns( 2 );
await expect( pageObject.productTemplate ).toHaveClass(
/columns-2/
@@ -194,6 +152,8 @@ test.describe( 'Product Collection', () => {
test( 'Order By - sort products by title in descending order correctly', async ( {
pageObject,
} ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
const sortedTitles = [
'WordPress Pennant',
'V-Neck T-Shirt',
@@ -217,6 +177,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on "on sale" status', async ( {
pageObject,
} ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
const allProducts = pageObject.products;
const salePoducts = pageObject.products.filter( {
hasText: 'Product on sale',
@@ -241,6 +203,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on selection in handpicked products option', async ( {
pageObject,
} ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
await pageObject.addFilter( 'Show Hand-picked Products' );
const filterName = 'Hand-picked Products';
@@ -261,6 +225,7 @@ test.describe( 'Product Collection', () => {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock();
+
await pageObject.addFilter( 'Keyword' );
await pageObject.setKeyword( 'Album' );
@@ -276,6 +241,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on category.', async ( {
pageObject,
} ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
const filterName = 'Product categories';
await pageObject.addFilter( 'Show product categories' );
await pageObject.setFilterComboboxValue( filterName, [
@@ -316,6 +283,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on tags.', async ( {
pageObject,
} ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
const filterName = 'Product tags';
await pageObject.addFilter( 'Show product tags' );
await pageObject.setFilterComboboxValue( filterName, [
@@ -336,6 +305,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on product attributes like color, size etc.', async ( {
pageObject,
} ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
await pageObject.addFilter( 'Show Product Attributes' );
await pageObject.setProductAttribute( 'Color', 'Green' );
@@ -353,6 +324,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on stock status (in stock, out of stock, or backorder).', async ( {
pageObject,
} ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
await pageObject.setFilterComboboxValue( 'Stock status', [
'Out of stock',
] );
@@ -371,6 +344,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on featured status.', async ( {
pageObject,
} ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Featured' );
@@ -389,6 +364,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on created date.', async ( {
pageObject,
} ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Created' );
@@ -416,6 +393,8 @@ test.describe( 'Product Collection', () => {
test( 'Products can be filtered based on price range.', async ( {
pageObject,
} ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.addFilter( 'Price Range' );
@@ -443,74 +422,132 @@ test.describe( 'Product Collection', () => {
await expect( pageObject.products ).toHaveCount( 4 );
} );
- test.describe( 'Sync with current template', () => {
- test( 'should not be visible on posts', async ( {
- pageObject,
- } ) => {
- await pageObject.createNewPostAndInsertBlock();
+ // See https://github.com/woocommerce/woocommerce/pull/49917
+ test( 'Price range is inclusive in both editor and frontend.', async ( {
+ page,
+ pageObject,
+ editor,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
- const sidebarSettings =
- await pageObject.locateSidebarSettings();
- await expect(
- sidebarSettings.locator(
- SELECTORS.inheritQueryFromTemplateControl
- )
- ).toBeHidden();
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await pageObject.addFilter( 'Price Range' );
+ await pageObject.setPriceRange( {
+ min: '45',
+ max: '55',
} );
- const archiveTemplates = [
+ // Wait for the products to be filtered.
+ await expect( pageObject.products ).not.toHaveCount( 9 );
+
+ await expect(
+ pageObject.products.filter( { hasText: '$45.00' } )
+ ).not.toHaveCount( 0 );
+ await expect(
+ pageObject.products.filter( { hasText: '$55.00' } )
+ ).not.toHaveCount( 0 );
+
+ // Reset the price range.
+ await pageObject.setPriceRange( {
+ min: '0',
+ max: '0',
+ } );
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await editor.insertBlock( {
+ name: 'woocommerce/filter-wrapper',
+ attributes: { filterType: 'price-filter' },
+ } );
+
+ await pageObject.publishAndGoToFrontend();
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await page
+ .getByRole( 'textbox', {
+ name: 'Filter products by minimum',
+ } )
+ .dblclick();
+ await page.keyboard.type( '45' );
+
+ await page
+ .getByRole( 'textbox', {
+ name: 'Filter products by maximum',
+ } )
+ .dblclick();
+ await page.keyboard.type( '55' );
+
+ await page.keyboard.press( 'Tab' );
+
+ // Wait for the products to be filtered.
+ await expect( pageObject.products ).not.toHaveCount( 9 );
+
+ await expect(
+ pageObject.products.filter( { hasText: '$45.00' } )
+ ).not.toHaveCount( 0 );
+ await expect(
+ pageObject.products.filter( { hasText: '$55.00' } )
+ ).not.toHaveCount( 0 );
+ } );
+
+ test.describe( '"Use page context" control', () => {
+ test( 'should be visible on posts', async ( { pageObject } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ await expect(
+ pageObject
+ .locateSidebarSettings()
+ .locator( SELECTORS.usePageContextControl )
+ ).toBeVisible();
+ } );
+
+ [
'woocommerce/woocommerce//archive-product',
'woocommerce/woocommerce//taxonomy-product_cat',
'woocommerce/woocommerce//taxonomy-product_tag',
'woocommerce/woocommerce//taxonomy-product_attribute',
'woocommerce/woocommerce//product-search-results',
- ];
-
- const nonArchiveTemplates = [
- 'woocommerce/woocommerce//single-product',
- 'twentytwentyfour//home',
- 'twentytwentyfour//index',
- ];
-
- archiveTemplates.map( async ( template ) => {
- test( `should be visible in archive template: ${ template }`, async ( {
+ ].forEach( ( slug ) => {
+ test( `should be visible in archive template: ${ slug }`, async ( {
pageObject,
editor,
} ) => {
- await pageObject.goToEditorTemplate( template );
+ await pageObject.goToEditorTemplate( slug );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate();
await pageObject.focusProductCollection();
await editor.openDocumentSettingsSidebar();
- const sidebarSettings =
- await pageObject.locateSidebarSettings();
await expect(
- sidebarSettings.locator(
- SELECTORS.inheritQueryFromTemplateControl
- )
+ pageObject
+ .locateSidebarSettings()
+ .locator( SELECTORS.usePageContextControl )
).toBeVisible();
} );
} );
- nonArchiveTemplates.map( async ( template ) => {
- test( `should not be visible in non-archive template: ${ template }`, async ( {
+ [
+ 'woocommerce/woocommerce//single-product',
+ 'twentytwentyfour//home',
+ 'twentytwentyfour//index',
+ ].forEach( ( slug ) => {
+ test( `should be visible in non-archive template: ${ slug }`, async ( {
pageObject,
editor,
} ) => {
- await pageObject.goToEditorTemplate( template );
+ await pageObject.goToEditorTemplate( slug );
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate();
await pageObject.focusProductCollection();
await editor.openDocumentSettingsSidebar();
- const sidebarSettings =
- await pageObject.locateSidebarSettings();
await expect(
- sidebarSettings.locator(
- SELECTORS.inheritQueryFromTemplateControl
- )
- ).toBeHidden();
+ pageObject
+ .locateSidebarSettings()
+ .locator( SELECTORS.usePageContextControl )
+ ).toBeVisible();
} );
} );
@@ -522,18 +559,15 @@ test.describe( 'Product Collection', () => {
await pageObject.focusProductCollection();
await editor.openDocumentSettingsSidebar();
- const sidebarSettings =
- await pageObject.locateSidebarSettings();
+ const sidebarSettings = pageObject.locateSidebarSettings();
// Inherit query from template should be visible & enabled by default
await expect(
- sidebarSettings.locator(
- SELECTORS.inheritQueryFromTemplateControl
- )
+ sidebarSettings.locator( SELECTORS.usePageContextControl )
).toBeVisible();
await expect(
sidebarSettings.locator(
- `${ SELECTORS.inheritQueryFromTemplateControl } input`
+ `${ SELECTORS.usePageContextControl } input`
)
).toBeChecked();
@@ -569,45 +603,136 @@ test.describe( 'Product Collection', () => {
).toBeChecked();
} );
- test( 'is enabled by default in 1st Product Collection and disabled in 2nd+', async ( {
+ test( 'is enabled by default unless already enabled elsewhere', async ( {
pageObject,
editor,
} ) => {
+ const productCollection = editor.canvas.getByLabel(
+ 'Block: Product Collection',
+ { exact: true }
+ );
+ const usePageContextToggle = pageObject
+ .locateSidebarSettings()
+ .locator( SELECTORS.usePageContextControl )
+ .locator( 'input' );
+
// First Product Catalog
// Option should be visible & ENABLED by default
await pageObject.goToEditorTemplate();
- await pageObject.focusProductCollection();
+ await editor.selectBlocks( productCollection.first() );
await editor.openDocumentSettingsSidebar();
- const sidebarSettings =
- await pageObject.locateSidebarSettings();
-
- await expect(
- sidebarSettings.locator(
- SELECTORS.inheritQueryFromTemplateControl
- )
- ).toBeVisible();
- await expect(
- sidebarSettings.locator(
- `${ SELECTORS.inheritQueryFromTemplateControl } input`
- )
- ).toBeChecked();
+ await expect( usePageContextToggle ).toBeChecked();
// Second Product Catalog
// Option should be visible & DISABLED by default
await pageObject.insertProductCollection();
await pageObject.chooseCollectionInTemplate( 'productCatalog' );
+ await editor.selectBlocks( productCollection.last() );
+
+ await expect( usePageContextToggle ).not.toBeChecked();
+
+ // Disable the option in the first Product Catalog
+ await editor.selectBlocks( productCollection.first() );
+ await usePageContextToggle.click();
+
+ // Third Product Catalog
+ // Option should be visible & ENABLED by default
+ await pageObject.insertProductCollection();
+ await pageObject.chooseCollectionInTemplate( 'productCatalog' );
+ await editor.selectBlocks( productCollection.last() );
+
+ await expect( usePageContextToggle ).toBeChecked();
+ } );
+
+ test( 'allows filtering in non-archive context', async ( {
+ pageObject,
+ editor,
+ page,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await pageObject.insertProductCollection();
+ await pageObject.chooseCollectionInPost( 'productCatalog' );
+
+ await expect( pageObject.products ).toHaveCount( 18 );
+
+ await page.getByLabel( 'Toggle block inserter' ).click();
+ await page.getByRole( 'tab', { name: 'Patterns' } ).click();
+ await page
+ .getByPlaceholder( 'Search' )
+ .fill( 'product filters' );
+ await page.getByLabel( 'Product Filters' ).click();
+
+ const postId = await editor.publishPost();
+ await page.goto( `/?p=${ postId }` );
+
+ const productCollection = page.locator(
+ '.wp-block-woocommerce-product-collection'
+ );
await expect(
- sidebarSettings.locator(
- SELECTORS.inheritQueryFromTemplateControl
- )
- ).toBeVisible();
+ productCollection.first().locator( SELECTORS.product )
+ ).toHaveCount( 9 );
await expect(
- sidebarSettings.locator(
- `${ SELECTORS.inheritQueryFromTemplateControl } input`
- )
- ).not.toBeChecked();
+ productCollection.last().locator( SELECTORS.product )
+ ).toHaveCount( 9 );
+
+ await page
+ .getByRole( 'textbox', {
+ name: 'Filter products by maximum',
+ } )
+ .dblclick();
+ await page.keyboard.type( '10' );
+ await page.keyboard.press( 'Tab' );
+
+ await expect(
+ productCollection.first().locator( SELECTORS.product )
+ ).toHaveCount( 1 );
+ await expect(
+ productCollection.last().locator( SELECTORS.product )
+ ).toHaveCount( 9 );
+ } );
+
+ test( 'correctly combines editor and front-end filters', async ( {
+ pageObject,
+ editor,
+ page,
+ } ) => {
+ await pageObject.createNewPostAndInsertBlock();
+
+ await expect( pageObject.products ).toHaveCount( 9 );
+
+ await pageObject.addFilter( 'Show product categories' );
+ await pageObject.setFilterComboboxValue( 'Product categories', [
+ 'Music',
+ ] );
+
+ await page.getByLabel( 'Toggle block inserter' ).click();
+ await page.getByRole( 'tab', { name: 'Patterns' } ).click();
+ await page
+ .getByPlaceholder( 'Search' )
+ .fill( 'product filters' );
+ await page.getByLabel( 'Product Filters' ).click();
+
+ await expect( pageObject.products ).toHaveCount( 2 );
+
+ const postId = await editor.publishPost();
+ await page.goto( `/?p=${ postId }` );
+
+ await expect( pageObject.products ).toHaveCount( 2 );
+
+ await page
+ .getByRole( 'textbox', {
+ name: 'Filter products by maximum',
+ } )
+ .dblclick();
+ await page.keyboard.type( '5' );
+ await page.keyboard.press( 'Tab' );
+
+ await expect( pageObject.products ).toHaveCount( 1 );
} );
} );
} );
@@ -827,17 +952,16 @@ test.describe( 'Product Collection', () => {
await expect( pageObject.products ).toHaveCount( 4 );
} );
- test( "Product Catalog Collection can be added in post and doesn't sync query with template", async ( {
+ test( 'Product Catalog Collection can be added in post and syncs query with template', async ( {
pageObject,
} ) => {
await pageObject.createNewPostAndInsertBlock( 'productCatalog' );
- const sidebarSettings = await pageObject.locateSidebarSettings();
- const input = sidebarSettings.locator(
- `${ SELECTORS.inheritQueryFromTemplateControl } input`
- );
+ const usePageContextToggle = pageObject
+ .locateSidebarSettings()
+ .locator( `${ SELECTORS.usePageContextControl } input` );
- await expect( input ).toBeHidden();
+ await expect( usePageContextToggle ).toBeVisible();
await expect( pageObject.products ).toHaveCount( 9 );
await pageObject.publishAndGoToFrontend();
@@ -862,9 +986,9 @@ test.describe( 'Product Collection', () => {
await pageObject.chooseCollectionInTemplate();
await editor.openDocumentSettingsSidebar();
- const sidebarSettings = await pageObject.locateSidebarSettings();
+ const sidebarSettings = pageObject.locateSidebarSettings();
const input = sidebarSettings.locator(
- `${ SELECTORS.inheritQueryFromTemplateControl } input`
+ `${ SELECTORS.usePageContextControl } input`
);
await expect( input ).toBeChecked();
@@ -894,8 +1018,7 @@ test.describe( 'Product Collection', () => {
test( 'On Sale', async ( { pageObject } ) => {
await pageObject.createNewPostAndInsertBlock( 'onSale' );
- const sidebarSettings =
- await pageObject.locateSidebarSettings();
+ const sidebarSettings = pageObject.locateSidebarSettings();
const input = sidebarSettings.getByLabel(
SELECTORS.onSaleControlLabel
);
@@ -905,8 +1028,7 @@ test.describe( 'Product Collection', () => {
test( 'Featured', async ( { pageObject } ) => {
await pageObject.createNewPostAndInsertBlock( 'featured' );
- const sidebarSettings =
- await pageObject.locateSidebarSettings();
+ const sidebarSettings = pageObject.locateSidebarSettings();
const input = sidebarSettings.getByLabel(
SELECTORS.featuredControlLabel
);
@@ -1350,42 +1472,124 @@ test.describe( 'Product Collection', () => {
test.describe( `${ templateTitle } template`, () => {
test( 'Product Collection block matches with classic template block', async ( {
pageObject,
+ requestUtils,
admin,
editor,
page,
} ) => {
- const getProductNamesFromClassicTemplate = async () => {
- const products = page.locator(
- '.woocommerce-loop-product__title'
- );
- await expect( products ).toHaveCount(
- expectedProductsCount
- );
- return products.allTextContents();
- };
+ await pageObject.refreshLocators( 'frontend' );
+
+ await page.goto( frontendPage );
+
+ const productCollectionProductNames =
+ await pageObject.getProductNames();
+
+ const template = await requestUtils.createTemplate(
+ 'wp_template',
+ {
+ slug,
+ title: 'classic template test',
+ content: 'howdy',
+ }
+ );
await admin.visitSiteEditor( {
- postId: `woocommerce/woocommerce//${ slug }`,
+ postId: template.id,
postType: 'wp_template',
canvas: 'edit',
} );
+
+ await expect(
+ editor.canvas.getByText( 'howdy' )
+ ).toBeVisible();
+
await editor.insertBlock( { name: legacyBlockName } );
- await editor.canvas.locator( 'body' ).click();
+
await editor.saveSiteEditorEntities( {
isOnlyCurrentEntityDirty: true,
} );
+
await page.goto( frontendPage );
- await pageObject.refreshLocators( 'frontend' );
- const classicProducts =
- await getProductNamesFromClassicTemplate();
- const productCollectionProducts =
- await pageObject.getProductNames();
+ const classicProducts = page.locator(
+ '.woocommerce-loop-product__title'
+ );
- expect( classicProducts ).toEqual( productCollectionProducts );
+ await expect( classicProducts ).toHaveCount(
+ expectedProductsCount
+ );
+
+ const classicProductsNames =
+ await classicProducts.allTextContents();
+
+ expect( classicProductsNames ).toEqual(
+ productCollectionProductNames
+ );
} );
} );
}
+ test.describe( 'Editor: In taxonomies templates', () => {
+ test( 'Products by specific category template displays products from this category', async ( {
+ admin,
+ page,
+ editor,
+ } ) => {
+ const expectedProducts = [
+ 'Hoodie',
+ 'Hoodie with Logo',
+ 'Hoodie with Zipper',
+ ];
+
+ await admin.visitSiteEditor( { path: '/wp_template' } );
+
+ await page
+ .getByRole( 'button', { name: 'Add New Template' } )
+ .click();
+ await page
+ .getByRole( 'button', { name: 'Products by Category' } )
+ .click();
+ await page
+ .getByRole( 'option', {
+ name: `Hoodies`,
+ } )
+ .click();
+ await page
+ .getByRole( 'option', { name: 'Fallback content' } )
+ .click();
+
+ const products = editor.canvas.getByLabel( 'Block: Title' );
+
+ await expect( products ).toHaveText( expectedProducts );
+ } );
+ test( 'Products by specific tag template displays products from this tag', async ( {
+ admin,
+ page,
+ editor,
+ } ) => {
+ const expectedProducts = [ 'Beanie', 'Hoodie' ];
+
+ await admin.visitSiteEditor( { path: '/wp_template' } );
+
+ await page
+ .getByRole( 'button', { name: 'Add New Template' } )
+ .click();
+ await page
+ .getByRole( 'button', { name: 'Products by Tag' } )
+ .click();
+ await page
+ .getByRole( 'option', {
+ name: `Recommended`,
+ } )
+ .click();
+ await page
+ .getByRole( 'option', { name: 'Fallback content' } )
+ .click();
+
+ const products = editor.canvas.getByLabel( 'Block: Title' );
+
+ await expect( products ).toHaveText( expectedProducts );
+ } );
+ } );
} );
/**
@@ -1460,7 +1664,7 @@ test.describe( 'Testing registerProductCollection', () => {
await pageObject.createNewPostAndInsertBlock(
'myCustomCollection'
);
- expect( pageObject.productTemplate ).not.toBeNull();
+
await expect( pageObject.products ).toHaveCount( 5 );
await expect( pageObject.productImages ).toHaveCount( 5 );
await expect( pageObject.productTitles ).toHaveCount( 5 );
@@ -1488,6 +1692,30 @@ test.describe( 'Testing registerProductCollection', () => {
.locator( 'visible=true' );
await expect( products ).toHaveCount( 5 );
} );
+
+ test( 'hideControls allows to hide filters', async ( {
+ pageObject,
+ page,
+ } ) => {
+ await pageObject.goToProductCatalogAndInsertCollection(
+ 'myCustomCollection'
+ );
+
+ const sidebarSettings = pageObject.locateSidebarSettings();
+ const onsaleControl = sidebarSettings.getByLabel(
+ SELECTORS.onSaleControlLabel
+ );
+ await expect( onsaleControl ).toBeHidden();
+
+ await page
+ .getByRole( 'button', { name: 'Filters options' } )
+ .click();
+ const keywordControl = page.getByRole( 'menuitemcheckbox', {
+ name: 'Keyword',
+ } );
+
+ await expect( keywordControl ).toBeHidden();
+ } );
} );
test.describe( 'My Custom Collection with Preview', () => {
@@ -1497,7 +1725,7 @@ test.describe( 'Testing registerProductCollection', () => {
await pageObject.createNewPostAndInsertBlock(
'myCustomCollectionWithPreview'
);
- expect( pageObject.productTemplate ).not.toBeNull();
+
await expect( pageObject.products ).toHaveCount( 9 );
await expect( pageObject.productImages ).toHaveCount( 9 );
await expect( pageObject.productTitles ).toHaveCount( 9 );
@@ -1556,7 +1784,7 @@ test.describe( 'Testing registerProductCollection', () => {
await pageObject.createNewPostAndInsertBlock(
'myCustomCollectionWithAdvancedPreview'
);
- expect( pageObject.productTemplate ).not.toBeNull();
+
await expect( pageObject.products ).toHaveCount( 9 );
await expect( pageObject.productImages ).toHaveCount( 9 );
await expect( pageObject.productTitles ).toHaveCount( 9 );
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts
index 189753da5cf..b154c1eb4c0 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-collection/product-collection.page.ts
@@ -38,7 +38,7 @@ export const SELECTORS = {
},
onSaleControlLabel: 'Show only products on sale',
featuredControlLabel: 'Show only featured products',
- inheritQueryFromTemplateControl:
+ usePageContextControl:
'.wc-block-product-collection__inherit-query-control',
shrinkColumnsToFit: 'Responsive',
productSearchLabel: 'Search',
@@ -220,6 +220,39 @@ class ProductCollectionPage {
return new URL( productResponse.url() );
}
+ async insertProductElements() {
+ // By default there are inner blocks:
+ // - woocommerce/product-image
+ // - core/post-title
+ // - woocommerce/product-price
+ // - woocommerce/product-button
+ // We're adding remaining ones
+ const productElements = [
+ { name: 'woocommerce/product-rating', attributes: {} },
+ { name: 'woocommerce/product-sku', attributes: {} },
+ { name: 'woocommerce/product-stock-indicator', attributes: {} },
+ { name: 'woocommerce/product-sale-badge', attributes: {} },
+ {
+ name: 'core/post-excerpt',
+ attributes: {
+ __woocommerceNamespace:
+ 'woocommerce/product-collection/product-summary',
+ },
+ },
+ {
+ name: 'core/post-terms',
+ attributes: { term: 'product_tag' },
+ },
+ {
+ name: 'core/post-terms',
+ attributes: { term: 'product_cat' },
+ },
+ ];
+
+ for ( const productElement of productElements ) {
+ await this.insertBlockInProductCollection( productElement );
+ }
+ }
async publishAndGoToFrontend() {
const postId = await this.editor.publishPost();
@@ -341,7 +374,7 @@ class ProductCollectionPage {
}
async setNumberOfColumns( numberOfColumns: number ) {
- const sidebarSettings = await this.locateSidebarSettings();
+ const sidebarSettings = this.locateSidebarSettings();
const inputField = sidebarSettings.getByRole( 'spinbutton', {
name: 'Columns',
} );
@@ -357,7 +390,7 @@ class ProductCollectionPage {
| 'popularity/desc'
| 'rating/desc'
) {
- const sidebarSettings = await this.locateSidebarSettings();
+ const sidebarSettings = this.locateSidebarSettings();
const orderByComboBox = sidebarSettings.getByRole( 'combobox', {
name: 'Order by',
} );
@@ -367,7 +400,7 @@ class ProductCollectionPage {
}
async getOrderByElement() {
- const sidebarSettings = await this.locateSidebarSettings();
+ const sidebarSettings = this.locateSidebarSettings();
return sidebarSettings.getByRole( 'combobox', {
name: 'Order by',
} );
@@ -390,7 +423,7 @@ class ProductCollectionPage {
onSale: true,
}
) {
- const sidebarSettings = await this.locateSidebarSettings();
+ const sidebarSettings = this.locateSidebarSettings();
const input = sidebarSettings.getByLabel(
SELECTORS.onSaleControlLabel
);
@@ -415,7 +448,7 @@ class ProductCollectionPage {
isLocatorsRefreshNeeded: true,
}
) {
- const sidebarSettings = await this.locateSidebarSettings();
+ const sidebarSettings = this.locateSidebarSettings();
const input = sidebarSettings.getByLabel(
SELECTORS.featuredControlLabel
);
@@ -442,7 +475,7 @@ class ProductCollectionPage {
const operatorSelector = SELECTORS.createdFilter.operator[ operator ];
const rangeSelector = SELECTORS.createdFilter.range[ range ];
- const sidebarSettings = await this.locateSidebarSettings();
+ const sidebarSettings = this.locateSidebarSettings();
const operatorButton = sidebarSettings.getByLabel( operatorSelector );
const rangeButton = sidebarSettings.getByLabel( rangeSelector );
@@ -454,7 +487,7 @@ class ProductCollectionPage {
const minInputSelector = SELECTORS.priceRangeFilter.min;
const maxInputSelector = SELECTORS.priceRangeFilter.max;
- const sidebarSettings = await this.locateSidebarSettings();
+ const sidebarSettings = this.locateSidebarSettings();
const minInput = sidebarSettings.getByLabel( minInputSelector );
const maxInput = sidebarSettings.getByLabel( maxInputSelector );
@@ -465,7 +498,7 @@ class ProductCollectionPage {
}
async setFilterComboboxValue( filterName: string, filterValue: string[] ) {
- const sidebarSettings = await this.locateSidebarSettings();
+ const sidebarSettings = this.locateSidebarSettings();
const input = sidebarSettings.getByLabel( filterName );
await input.click();
@@ -494,7 +527,7 @@ class ProductCollectionPage {
}
async setKeyword( keyword: string ) {
- const sidebarSettings = await this.locateSidebarSettings();
+ const sidebarSettings = this.locateSidebarSettings();
const input = sidebarSettings.getByLabel( 'Keyword' );
await input.clear();
await input.fill( keyword );
@@ -557,7 +590,7 @@ class ProductCollectionPage {
}
async setShrinkColumnsToFit( value = true ) {
- const sidebarSettings = await this.locateSidebarSettings();
+ const sidebarSettings = this.locateSidebarSettings();
const input = sidebarSettings.getByLabel(
SELECTORS.shrinkColumnsToFit
);
@@ -569,7 +602,7 @@ class ProductCollectionPage {
}
async setProductAttribute( attribute: 'Color' | 'Size', value: string ) {
- const sidebarSettings = await this.locateSidebarSettings();
+ const sidebarSettings = this.locateSidebarSettings();
const productAttributesContainer = sidebarSettings.locator(
'.woocommerce-product-attributes'
@@ -598,9 +631,9 @@ class ProductCollectionPage {
}
async setInheritQueryFromTemplate( inheritQueryFromTemplate: boolean ) {
- const sidebarSettings = await this.locateSidebarSettings();
+ const sidebarSettings = this.locateSidebarSettings();
const input = sidebarSettings.locator(
- `${ SELECTORS.inheritQueryFromTemplateControl } input`
+ `${ SELECTORS.usePageContextControl } input`
);
if ( inheritQueryFromTemplate ) {
await input.check();
@@ -654,7 +687,7 @@ class ProductCollectionPage {
/**
* Locators
*/
- async locateSidebarSettings() {
+ locateSidebarSettings() {
return this.page.getByRole( 'region', {
name: 'Editor settings',
} );
@@ -670,7 +703,7 @@ class ProductCollectionPage {
async getProductNames() {
const products = this.page.locator( '.wp-block-post-title' );
- return products.allTextContents();
+ return await products.allTextContents();
}
/**
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-navigation.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-navigation.block_theme.spec.ts
index 431aa08255f..9693641793d 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-navigation.block_theme.spec.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-navigation.block_theme.spec.ts
@@ -5,7 +5,7 @@ import { test, expect } from '@woocommerce/e2e-utils';
const blockData = {
name: 'woocommerce/product-filters-overlay-navigation',
- title: 'Navigation (Experimental)',
+ title: 'Overlay Navigation (Experimental)',
selectors: {
frontend: {},
editor: {
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts
index cd9221ec9e4..9601e004dea 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts
@@ -10,14 +10,34 @@ test.describe( 'Filters Overlay Template Part', () => {
);
await admin.visitSiteEditor( {
postType: 'wp_template_part',
+ postId: 'woocommerce/woocommerce//product-filters-overlay',
+ canvas: 'edit',
} );
} );
- test( 'should be visible', async ( { page } ) => {
+ test( 'should be visible in the template parts list', async ( {
+ page,
+ admin,
+ } ) => {
+ await admin.visitSiteEditor( {
+ postType: 'wp_template_part',
+ } );
const block = page
.getByLabel( 'Patterns content' )
.getByText( 'Filters Overlay' )
.and( page.getByRole( 'button' ) );
await expect( block ).toBeVisible();
} );
+
+ test( 'should render the correct inner blocks', async ( { editor } ) => {
+ const productFiltersTemplatePart = editor.canvas
+ .locator( '[data-type="core/template-part"]' )
+ .filter( {
+ has: editor.canvas.getByLabel(
+ 'Block: Product Filters (Experimental)'
+ ),
+ } );
+
+ await expect( productFiltersTemplatePart ).toBeVisible();
+ } );
} );
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts
new file mode 100644
index 00000000000..996af1d0774
--- /dev/null
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts
@@ -0,0 +1,154 @@
+/**
+ * External dependencies
+ */
+import { test as base, expect } from '@woocommerce/e2e-utils';
+
+/**
+ * Internal dependencies
+ */
+import { ProductFiltersPage } from './product-filters.page';
+
+const blockData = {
+ name: 'woocommerce/product-filter-attribute',
+ selectors: {
+ frontend: {},
+ editor: {
+ settings: {},
+ },
+ },
+ slug: 'archive-product',
+};
+
+const test = base.extend< { pageObject: ProductFiltersPage } >( {
+ pageObject: async ( { page, editor, frontendUtils }, use ) => {
+ const pageObject = new ProductFiltersPage( {
+ page,
+ editor,
+ frontendUtils,
+ } );
+ await use( pageObject );
+ },
+} );
+
+test.describe( `${ blockData.name }`, () => {
+ test.beforeEach( async ( { admin, requestUtils } ) => {
+ await requestUtils.activatePlugin(
+ 'woocommerce-blocks-test-enable-experimental-features'
+ );
+ await admin.visitSiteEditor( {
+ postId: `woocommerce/woocommerce//${ blockData.slug }`,
+ postType: 'wp_template',
+ canvas: 'edit',
+ } );
+ } );
+
+ test( 'should display the correct inspector style controls', async ( {
+ editor,
+ pageObject,
+ } ) => {
+ await pageObject.addProductFiltersBlock( { cleanContent: true } );
+
+ const block = editor.canvas
+ .getByLabel( 'Block: Attribute (Experimental)' )
+ .getByLabel( 'Block: Filter Options' );
+
+ await expect( block ).toBeVisible();
+
+ await block.click();
+ await editor.openDocumentSettingsSidebar();
+ await editor.page.getByRole( 'tab', { name: 'Styles' } ).click();
+
+ await expect(
+ editor.page.getByText( 'ColorAll options are currently hidden' )
+ ).toBeVisible();
+ await expect(
+ editor.page.getByText(
+ 'TypographyAll options are currently hidden'
+ )
+ ).toBeVisible();
+ await expect(
+ editor.page.getByText(
+ 'DimensionsAll options are currently hidden'
+ )
+ ).toBeVisible();
+ await expect(
+ editor.page.getByText( 'DisplayListChips' )
+ ).toBeVisible();
+ } );
+
+ test( 'should display the correct inspector setting controls', async ( {
+ editor,
+ pageObject,
+ } ) => {
+ await pageObject.addProductFiltersBlock( { cleanContent: true } );
+
+ const block = editor.canvas
+ .getByLabel( 'Block: Attribute (Experimental)' )
+ .getByLabel( 'Block: Filter Options' );
+
+ await expect( block ).toBeVisible();
+
+ await editor.openDocumentSettingsSidebar();
+ await block.click();
+
+ await expect(
+ editor.page.getByLabel( 'Editor settings' ).getByRole( 'button', {
+ name: 'Attribute',
+ exact: true,
+ } )
+ ).toBeVisible();
+ await expect(
+ editor.page
+ .getByLabel( 'Editor settings' )
+ .getByRole( 'button', { name: 'Settings', exact: true } )
+ ).toBeVisible();
+ await expect( editor.page.getByText( 'Sort order' ) ).toBeVisible();
+ await expect( editor.page.getByText( 'LogicAnyAll' ) ).toBeVisible();
+ } );
+
+ test( 'should dynamically set block title and heading based on the selected attribute', async ( {
+ editor,
+ pageObject,
+ } ) => {
+ await pageObject.addProductFiltersBlock( { cleanContent: true } );
+
+ const block = editor.canvas
+ .getByLabel( 'Block: Attribute (Experimental)' )
+ .getByLabel( 'Block: Filter Options' );
+
+ await editor.openDocumentSettingsSidebar();
+ await block.click();
+
+ await editor.page
+ .getByRole( 'tabpanel' )
+ .getByRole( 'combobox' )
+ .first()
+ .click();
+ await editor.page
+ .getByRole( 'option', { name: 'Size', exact: true } )
+ .click();
+
+ await pageObject.page.getByLabel( 'Document Overview' ).click();
+ const listView = pageObject.page.getByLabel( 'List View' );
+
+ await expect( listView ).toBeVisible();
+
+ const productFilterAttributeSizeBlockListItem = listView.getByText(
+ 'Size (Experimental)' // it must select the attribute with the highest product count
+ );
+ await expect( productFilterAttributeSizeBlockListItem ).toBeVisible();
+
+ const productFilterAttributeWrapperBlock = editor.canvas.getByLabel(
+ 'Block: Attribute (Experimental)'
+ );
+ await editor.selectBlocks( productFilterAttributeWrapperBlock );
+ await expect( productFilterAttributeWrapperBlock ).toBeVisible();
+
+ const productFilterAttributeBlockHeading =
+ productFilterAttributeWrapperBlock.getByText( 'Size', {
+ exact: true,
+ } );
+
+ await expect( productFilterAttributeBlockHeading ).toBeVisible();
+ } );
+} );
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts
new file mode 100644
index 00000000000..ef76aaeb74f
--- /dev/null
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts
@@ -0,0 +1,38 @@
+/**
+ * External dependencies
+ */
+import { test, expect } from '@woocommerce/e2e-utils';
+
+test.describe( 'Product Filters Template Part', () => {
+ test.beforeEach( async ( { admin, requestUtils } ) => {
+ await requestUtils.activatePlugin(
+ 'woocommerce-blocks-test-enable-experimental-features'
+ );
+ await admin.visitSiteEditor( {
+ postType: 'wp_template_part',
+ postId: 'woocommerce/woocommerce//product-filters',
+ canvas: 'edit',
+ } );
+ } );
+
+ test( 'should be visible in the templates part list', async ( {
+ page,
+ admin,
+ } ) => {
+ await admin.visitSiteEditor( {
+ postType: 'wp_template_part',
+ } );
+ const templatePart = page
+ .getByLabel( 'Patterns content' )
+ .getByText( 'Product Filters (Experimental)', { exact: true } )
+ .and( page.getByRole( 'button' ) );
+ await expect( templatePart ).toBeVisible();
+ } );
+
+ test( 'should render the Product Filters block', async ( { editor } ) => {
+ const productFiltersBlock = editor.canvas.getByLabel(
+ 'Block: Product Filters (Experimental)'
+ );
+ await expect( productFiltersBlock ).toBeVisible();
+ } );
+} );
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts
index 45b2084004b..1808a3ff867 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts
@@ -57,42 +57,53 @@ test.describe( `${ blockData.name }`, () => {
);
await expect( block ).toBeVisible();
- const filtersbBlockHeading = block.getByRole( 'document', {
- name: 'Filters',
- } );
- await expect( filtersbBlockHeading ).toBeVisible();
-
const activeHeading = block.getByText( 'Active', { exact: true } );
- const activeFilterBlock = block.getByLabel(
- 'Block: Product Filter: Active'
- );
+ const activeFilterBlock = block
+ .getByLabel( 'Block: Filter Options' )
+ .and(
+ editor.canvas.locator(
+ '[data-type="woocommerce/product-filter-active"]'
+ )
+ );
await expect( activeHeading ).toBeVisible();
await expect( activeFilterBlock ).toBeVisible();
const priceHeading = block.getByText( 'Price', {
exact: true,
} );
- const priceFilterBlock = block.getByLabel(
- 'Block: Product Filter: Price'
- );
+ const priceFilterBlock = block
+ .getByLabel( 'Block: Filter Options' )
+ .and(
+ editor.canvas.locator(
+ '[data-type="woocommerce/product-filter-price"]'
+ )
+ );
await expect( priceHeading ).toBeVisible();
await expect( priceFilterBlock ).toBeVisible();
const statusHeading = block.getByText( 'Status', {
exact: true,
} );
- const statusFilterBlock = block.getByLabel(
- 'Block: Product Filter: Stock'
- );
+ const statusFilterBlock = block
+ .getByLabel( 'Block: Filter Options' )
+ .and(
+ editor.canvas.locator(
+ '[data-type="woocommerce/product-filter-stock-status"]'
+ )
+ );
await expect( statusHeading ).toBeVisible();
await expect( statusFilterBlock ).toBeVisible();
const colorHeading = block.getByText( 'Color', {
exact: true,
} );
- const colorFilterBlock = block.getByLabel(
- 'Block: Product Filter: Attribute (Experimental)'
- );
+ const colorFilterBlock = block
+ .getByLabel( 'Block: Filter Options' )
+ .and(
+ editor.canvas.locator(
+ '[data-type="woocommerce/product-filter-attribute"]'
+ )
+ );
const expectedColorFilterOptions = [
'Blue',
'Green',
@@ -112,9 +123,13 @@ test.describe( `${ blockData.name }`, () => {
const ratingHeading = block.getByText( 'Rating', {
exact: true,
} );
- const ratingFilterBlock = block.getByLabel(
- 'Block: Product Filter: Rating (Experimental)'
- );
+ const ratingFilterBlock = block
+ .getByLabel( 'Block: Filter Options' )
+ .and(
+ editor.canvas.locator(
+ '[data-type="woocommerce/product-filter-rating"]'
+ )
+ );
await expect( ratingHeading ).toBeVisible();
await expect( ratingFilterBlock ).toBeVisible();
} );
@@ -241,6 +256,64 @@ test.describe( `${ blockData.name }`, () => {
await expect(
editor.page.getByText( 'LayoutJustificationOrientation' )
).toBeVisible();
+
+ // Overlay settings
+ const overlayModeSettings = [ 'Never', 'Mobile', 'Always' ];
+ const overlayButtonSettings = [
+ 'Label and icon',
+ 'Label only',
+ 'Icon only',
+ ];
+ const overlayIconsSettings = [
+ 'Filter icon 1',
+ 'Filter icon 2',
+ 'Filter icon 3',
+ 'Filter icon 4',
+ ];
+
+ await expect( editor.page.getByText( 'Overlay' ) ).toBeVisible();
+
+ for ( const mode of overlayModeSettings ) {
+ await expect( editor.page.getByText( mode ) ).toBeVisible();
+ }
+
+ await editor.page.getByLabel( 'Mobile' ).click();
+ await expect( editor.page.getByText( 'BUTTON' ) ).toBeVisible();
+
+ for ( const mode of overlayButtonSettings ) {
+ await expect( editor.page.getByText( mode ) ).toBeVisible();
+ }
+
+ for ( const mode of overlayIconsSettings ) {
+ await expect( editor.page.getByLabel( mode ) ).toBeVisible();
+ }
+
+ await expect( editor.page.getByText( 'ICON SIZE' ) ).toBeVisible();
+ await expect( editor.page.getByText( 'Edit overlay' ) ).toBeVisible();
+
+ await editor.page.getByLabel( 'Always' ).click();
+
+ await expect( editor.page.getByText( 'BUTTON' ) ).toBeHidden();
+
+ for ( const mode of overlayButtonSettings ) {
+ await expect( editor.page.getByText( mode ) ).toBeHidden();
+ }
+
+ for ( const mode of overlayIconsSettings ) {
+ await expect( editor.page.getByLabel( mode ) ).toBeHidden();
+ }
+
+ await expect( editor.page.getByText( 'Edit overlay' ) ).toBeVisible();
+
+ await editor.page.getByLabel( 'Mobile' ).click();
+
+ await editor.page.locator( 'input[value="label"]' ).click();
+
+ for ( const mode of overlayIconsSettings ) {
+ await expect( editor.page.getByLabel( mode ) ).toBeHidden();
+ }
+
+ await expect( editor.page.getByText( 'Edit overlay' ) ).toBeVisible();
} );
test( 'Layout > default to vertical stretch', async ( {
@@ -366,68 +439,4 @@ test.describe( `${ blockData.name }`, () => {
block.locator( blockData.selectors.editor.layoutWrapper )
).toHaveCSS( 'gap', '0px' );
} );
-
- test.describe( 'product-filter-attribute', () => {
- test( 'should dynamically set block title and heading based on the selected attribute', async ( {
- editor,
- pageObject,
- } ) => {
- await pageObject.addProductFiltersBlock( { cleanContent: true } );
-
- await pageObject.page.getByLabel( 'Document Overview' ).click();
- const listView = pageObject.page.getByLabel( 'List View' );
-
- const productFiltersBlockListItem = listView.getByRole( 'link', {
- name: 'Product Filters (Experimental)',
- } );
- await expect( productFiltersBlockListItem ).toBeVisible();
- const listViewExpander =
- pageObject.page.getByTestId( 'list-view-expander' );
- const listViewExpanderIcon = listViewExpander.locator( 'svg' );
-
- await listViewExpanderIcon.click();
-
- const productFilterAttributeColorBlockListItem = listView.getByText(
- 'Color (Experimental)' // it must select the attribute with the highest product count
- );
- await expect(
- productFilterAttributeColorBlockListItem
- ).toBeVisible();
-
- const productFilterAttributeBlock = editor.canvas.getByLabel(
- 'Block: Product Filter: Attribute (Experimental)'
- );
- await editor.selectBlocks( productFilterAttributeBlock );
- await editor.clickBlockToolbarButton( 'Edit' );
- await editor.canvas
- .locator( 'label' )
- .filter( { hasText: 'Size' } )
- .click();
- await editor.canvas.getByRole( 'button', { name: 'Done' } ).click();
-
- await expect(
- productFilterAttributeColorBlockListItem
- ).toBeHidden();
-
- const productFilterAttributeSizeBlockListItem = listView.getByText(
- 'Size (Experimental)' // it must select the attribute with the highest product count
- );
- await expect(
- productFilterAttributeSizeBlockListItem
- ).toBeVisible();
-
- const productFilterAttributeWrapperBlock = editor.canvas.getByLabel(
- 'Block: Attribute (Experimental)'
- );
- await editor.selectBlocks( productFilterAttributeWrapperBlock );
- await expect( productFilterAttributeWrapperBlock ).toBeVisible();
-
- const productFilterAttributeBlockHeading =
- productFilterAttributeWrapperBlock.getByText( 'Size', {
- exact: true,
- } );
-
- await expect( productFilterAttributeBlockHeading ).toBeVisible();
- } );
- } );
} );
diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/products-by-attribute/products-by-attribute.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/products-by-attribute/products-by-attribute.block_theme.spec.ts
index 0ca735b4af8..1abba804497 100644
--- a/plugins/woocommerce-blocks/tests/e2e/tests/products-by-attribute/products-by-attribute.block_theme.spec.ts
+++ b/plugins/woocommerce-blocks/tests/e2e/tests/products-by-attribute/products-by-attribute.block_theme.spec.ts
@@ -28,4 +28,28 @@ test.describe( `${ blockData.slug } Block`, () => {
blockLocatorFrontend.getByRole( 'listitem' )
).toHaveCount( 9 );
} );
+
+ test( 'can change attributes from the sidebar', async ( {
+ editor,
+ admin,
+ frontendUtils,
+ page,
+ } ) => {
+ await admin.createNewPost();
+ await editor.insertBlock( { name: blockData.slug } );
+ const blockLocator = await editor.getBlockByName( blockData.slug );
+ await blockLocator.getByText( 'Color' ).click();
+ await blockLocator.getByText( 'Done' ).click();
+ await page.getByText( 'Filter by Product Attribute' ).click();
+ await page.getByText( 'Color' ).click();
+ await page.getByText( 'Size' ).click();
+ await expect( blockLocator.getByRole( 'listitem' ) ).toHaveCount( 1 );
+ await editor.publishAndVisitPost();
+ const blockLocatorFrontend = await frontendUtils.getBlockByName(
+ blockData.slug
+ );
+ await expect(
+ blockLocatorFrontend.getByRole( 'listitem' )
+ ).toHaveCount( 1 );
+ } );
} );
diff --git a/plugins/woocommerce-blocks/tests/js/jest.config.json b/plugins/woocommerce-blocks/tests/js/jest.config.json
index c91bf754a0d..e6a18306fee 100644
--- a/plugins/woocommerce-blocks/tests/js/jest.config.json
+++ b/plugins/woocommerce-blocks/tests/js/jest.config.json
@@ -45,9 +45,12 @@
"/tests/js/setup-after-env.ts"
],
"testPathIgnorePatterns": [
- "/tests/",
+ "/bin/",
+ "/build/",
+ "/docs/",
"/node_modules/",
- "/vendor/"
+ "/vendor/",
+ "/tests/"
],
"transformIgnorePatterns": [ "node_modules/?!(simple-html-tokenizer|is-plain-obj|is-plain-object|memize)" ],
"testEnvironment": "jsdom",
@@ -55,5 +58,6 @@
"transform": {
"^.+\\.(js|ts|tsx)$": "/tests/js/jestPreprocess.js"
},
- "verbose": true
+ "verbose": true,
+ "cacheDirectory": "/../../node_modules/.cache/jest"
}
diff --git a/plugins/woocommerce-blocks/webpack.config.js b/plugins/woocommerce-blocks/webpack.config.js
index ddb7942d125..d1a62c1e3fa 100644
--- a/plugins/woocommerce-blocks/webpack.config.js
+++ b/plugins/woocommerce-blocks/webpack.config.js
@@ -11,6 +11,7 @@ const {
getSiteEditorConfig,
getStylingConfig,
getInteractivityAPIConfig,
+ getCartAndCheckoutFrontendConfig,
} = require( './bin/webpack-configs.js' );
// Only options shared between all configs should be defined here.
@@ -34,6 +35,11 @@ const sharedConfig = {
devtool: NODE_ENV === 'development' ? 'source-map' : false,
};
+const CartAndCheckoutFrontendConfig = {
+ ...sharedConfig,
+ ...getCartAndCheckoutFrontendConfig( { alias: getAlias() } ),
+};
+
// Core config for shared libraries.
const CoreConfig = {
...sharedConfig,
@@ -95,6 +101,7 @@ const SiteEditorConfig = {
};
module.exports = [
+ CartAndCheckoutFrontendConfig,
CoreConfig,
MainConfig,
FrontendConfig,
diff --git a/plugins/woocommerce/.wp-env.json b/plugins/woocommerce/.wp-env.json
index bc65ab54f71..9dd14efb4be 100644
--- a/plugins/woocommerce/.wp-env.json
+++ b/plugins/woocommerce/.wp-env.json
@@ -1,8 +1,7 @@
{
+ "core": null,
"phpVersion": "7.4",
- "plugins": [
- "."
- ],
+ "plugins": [ "." ],
"config": {
"JETPACK_AUTOLOAD_DEV": true,
"WP_DEBUG_LOG": true,
diff --git a/plugins/woocommerce/assets/images/onboarding/wcpay-bnpl.svg b/plugins/woocommerce/assets/images/onboarding/wcpay-bnpl.svg
new file mode 100644
index 00000000000..4d5c6e4becd
--- /dev/null
+++ b/plugins/woocommerce/assets/images/onboarding/wcpay-bnpl.svg
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/woocommerce/bin/build-zip.sh b/plugins/woocommerce/bin/build-zip.sh
index 1e3f2050f21..6fd67f824fd 100755
--- a/plugins/woocommerce/bin/build-zip.sh
+++ b/plugins/woocommerce/bin/build-zip.sh
@@ -14,7 +14,7 @@ echo "Cleaning up assets..."
find "$PROJECT_PATH/assets/css/." ! -name '.gitkeep' -type f -exec rm -f {} + && find "$PROJECT_PATH/assets/client/." ! -name '.gitkeep' -type f -exec rm -f {} + && find "$PROJECT_PATH/assets/js/." ! -name '.gitkeep' -type f -exec rm -f {} +
echo "Installing PHP and JS dependencies..."
-pnpm install
+pnpm install --frozen-lockfile
echo "Running JS Build..."
if [ -z "${NODE_ENV}" ]; then
diff --git a/plugins/woocommerce/bin/composer/mozart/composer.lock b/plugins/woocommerce/bin/composer/mozart/composer.lock
index f6fafac9eec..dfd7ff4781c 100644
--- a/plugins/woocommerce/bin/composer/mozart/composer.lock
+++ b/plugins/woocommerce/bin/composer/mozart/composer.lock
@@ -13,12 +13,12 @@
"source": {
"type": "git",
"url": "https://github.com/coenjacobs/mozart.git",
- "reference": "4f9d00fbc3b3e39f4e334434fe058e516ad82291"
+ "reference": "acfe1deacfed2b68c1b528e3916b0373b55c47fa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/coenjacobs/mozart/zipball/4f9d00fbc3b3e39f4e334434fe058e516ad82291",
- "reference": "4f9d00fbc3b3e39f4e334434fe058e516ad82291",
+ "url": "https://api.github.com/repos/coenjacobs/mozart/zipball/acfe1deacfed2b68c1b528e3916b0373b55c47fa",
+ "reference": "acfe1deacfed2b68c1b528e3916b0373b55c47fa",
"shasum": ""
},
"require": {
@@ -66,7 +66,7 @@
"type": "github"
}
],
- "time": "2022-10-22T08:08:20+00:00"
+ "time": "2024-07-16T13:06:14+00:00"
},
{
"name": "league/flysystem",
@@ -268,16 +268,16 @@
},
{
"name": "symfony/console",
- "version": "v5.4.35",
+ "version": "v5.4.41",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931"
+ "reference": "6473d441a913cb997123b59ff2dbe3d1cf9e11ba"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/dbdf6adcb88d5f83790e1efb57ef4074309d3931",
- "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931",
+ "url": "https://api.github.com/repos/symfony/console/zipball/6473d441a913cb997123b59ff2dbe3d1cf9e11ba",
+ "reference": "6473d441a913cb997123b59ff2dbe3d1cf9e11ba",
"shasum": ""
},
"require": {
@@ -347,7 +347,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v5.4.35"
+ "source": "https://github.com/symfony/console/tree/v5.4.41"
},
"funding": [
{
@@ -363,20 +363,20 @@
"type": "tidelift"
}
],
- "time": "2024-01-23T14:28:09+00:00"
+ "time": "2024-06-28T07:48:55+00:00"
},
{
"name": "symfony/deprecation-contracts",
- "version": "v2.5.2",
+ "version": "v2.5.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
- "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66"
+ "reference": "80d075412b557d41002320b96a096ca65aa2c98d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
- "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d",
+ "reference": "80d075412b557d41002320b96a096ca65aa2c98d",
"shasum": ""
},
"require": {
@@ -414,7 +414,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2"
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3"
},
"funding": [
{
@@ -430,20 +430,20 @@
"type": "tidelift"
}
],
- "time": "2022-01-02T09:53:40+00:00"
+ "time": "2023-01-24T14:02:46+00:00"
},
{
"name": "symfony/finder",
- "version": "v5.4.35",
+ "version": "v5.4.40",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "abe6d6f77d9465fed3cd2d029b29d03b56b56435"
+ "reference": "f51cff4687547641c7d8180d74932ab40b2205ce"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/abe6d6f77d9465fed3cd2d029b29d03b56b56435",
- "reference": "abe6d6f77d9465fed3cd2d029b29d03b56b56435",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/f51cff4687547641c7d8180d74932ab40b2205ce",
+ "reference": "f51cff4687547641c7d8180d74932ab40b2205ce",
"shasum": ""
},
"require": {
@@ -477,7 +477,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v5.4.35"
+ "source": "https://github.com/symfony/finder/tree/v5.4.40"
},
"funding": [
{
@@ -493,20 +493,20 @@
"type": "tidelift"
}
],
- "time": "2024-01-23T13:51:25+00:00"
+ "time": "2024-05-31T14:33:22+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.29.0",
+ "version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
+ "reference": "0424dff1c58f028c451efff2045f5d92410bd540"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
- "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
+ "reference": "0424dff1c58f028c451efff2045f5d92410bd540",
"shasum": ""
},
"require": {
@@ -556,7 +556,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
},
"funding": [
{
@@ -572,20 +572,20 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.29.0",
+ "version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f"
+ "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f",
- "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a",
+ "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a",
"shasum": ""
},
"require": {
@@ -634,7 +634,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0"
},
"funding": [
{
@@ -650,20 +650,20 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.29.0",
+ "version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
- "reference": "bc45c394692b948b4d383a08d7753968bed9a83d"
+ "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d",
- "reference": "bc45c394692b948b4d383a08d7753968bed9a83d",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb",
+ "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb",
"shasum": ""
},
"require": {
@@ -715,7 +715,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0"
},
"funding": [
{
@@ -731,20 +731,20 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.29.0",
+ "version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
+ "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
- "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
+ "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"shasum": ""
},
"require": {
@@ -795,7 +795,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
},
"funding": [
{
@@ -811,20 +811,20 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-06-19T12:30:46+00:00"
},
{
"name": "symfony/polyfill-php73",
- "version": "v1.29.0",
+ "version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php73.git",
- "reference": "21bd091060673a1177ae842c0ef8fe30893114d2"
+ "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2",
- "reference": "21bd091060673a1177ae842c0ef8fe30893114d2",
+ "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/ec444d3f3f6505bb28d11afa41e75faadebc10a1",
+ "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1",
"shasum": ""
},
"require": {
@@ -871,7 +871,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-php73/tree/v1.30.0"
},
"funding": [
{
@@ -887,20 +887,20 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.29.0",
+ "version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b"
+ "reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
- "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
+ "reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
"shasum": ""
},
"require": {
@@ -951,7 +951,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
},
"funding": [
{
@@ -967,20 +967,20 @@
"type": "tidelift"
}
],
- "time": "2024-01-29T20:11:03+00:00"
+ "time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/service-contracts",
- "version": "v2.5.2",
+ "version": "v2.5.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
- "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c"
+ "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c",
- "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a2329596ddc8fd568900e3fc76cba42489ecc7f3",
+ "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3",
"shasum": ""
},
"require": {
@@ -1034,7 +1034,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/service-contracts/tree/v2.5.2"
+ "source": "https://github.com/symfony/service-contracts/tree/v2.5.3"
},
"funding": [
{
@@ -1050,20 +1050,20 @@
"type": "tidelift"
}
],
- "time": "2022-05-30T19:17:29+00:00"
+ "time": "2023-04-21T15:04:16+00:00"
},
{
"name": "symfony/string",
- "version": "v5.4.35",
+ "version": "v5.4.41",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "c209c4d0559acce1c9a2067612cfb5d35756edc2"
+ "reference": "065a9611e0b1fd2197a867e1fb7f2238191b7096"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/c209c4d0559acce1c9a2067612cfb5d35756edc2",
- "reference": "c209c4d0559acce1c9a2067612cfb5d35756edc2",
+ "url": "https://api.github.com/repos/symfony/string/zipball/065a9611e0b1fd2197a867e1fb7f2238191b7096",
+ "reference": "065a9611e0b1fd2197a867e1fb7f2238191b7096",
"shasum": ""
},
"require": {
@@ -1120,7 +1120,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v5.4.35"
+ "source": "https://github.com/symfony/string/tree/v5.4.41"
},
"funding": [
{
@@ -1136,7 +1136,7 @@
"type": "tidelift"
}
],
- "time": "2024-01-23T13:51:25+00:00"
+ "time": "2024-06-28T09:20:55+00:00"
}
],
"aliases": [],
diff --git a/plugins/woocommerce/bin/composer/phpcs/composer.lock b/plugins/woocommerce/bin/composer/phpcs/composer.lock
index 7f36c631fbc..17836dc2f60 100644
--- a/plugins/woocommerce/bin/composer/phpcs/composer.lock
+++ b/plugins/woocommerce/bin/composer/phpcs/composer.lock
@@ -149,28 +149,28 @@
},
{
"name": "phpcompatibility/phpcompatibility-paragonie",
- "version": "1.3.2",
+ "version": "1.3.3",
"source": {
"type": "git",
"url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git",
- "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26"
+ "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26",
- "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26",
+ "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/293975b465e0e709b571cbf0c957c6c0a7b9a2ac",
+ "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac",
"shasum": ""
},
"require": {
"phpcompatibility/php-compatibility": "^9.0"
},
"require-dev": {
- "dealerdirect/phpcodesniffer-composer-installer": "^0.7",
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"paragonie/random_compat": "dev-master",
"paragonie/sodium_compat": "dev-master"
},
"suggest": {
- "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.",
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.",
"roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues."
},
"type": "phpcodesniffer-standard",
@@ -200,22 +200,37 @@
],
"support": {
"issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues",
+ "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy",
"source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie"
},
- "time": "2022-10-25T01:46:02+00:00"
+ "funding": [
+ {
+ "url": "https://github.com/PHPCompatibility",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2024-04-24T21:30:46+00:00"
},
{
"name": "phpcompatibility/phpcompatibility-wp",
- "version": "2.1.4",
+ "version": "2.1.5",
"source": {
"type": "git",
"url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git",
- "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5"
+ "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5",
- "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5",
+ "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/01c1ff2704a58e46f0cb1ca9d06aee07b3589082",
+ "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082",
"shasum": ""
},
"require": {
@@ -223,10 +238,10 @@
"phpcompatibility/phpcompatibility-paragonie": "^1.0"
},
"require-dev": {
- "dealerdirect/phpcodesniffer-composer-installer": "^0.7"
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0"
},
"suggest": {
- "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.",
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.",
"roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues."
},
"type": "phpcodesniffer-standard",
@@ -255,9 +270,24 @@
],
"support": {
"issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues",
+ "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy",
"source": "https://github.com/PHPCompatibility/PHPCompatibilityWP"
},
- "time": "2022-10-24T09:00:36+00:00"
+ "funding": [
+ {
+ "url": "https://github.com/PHPCompatibility",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2024-04-24T21:37:59+00:00"
},
{
"name": "phpcsstandards/phpcsextra",
@@ -339,22 +369,22 @@
},
{
"name": "phpcsstandards/phpcsutils",
- "version": "1.0.9",
+ "version": "1.0.12",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHPCSUtils.git",
- "reference": "908247bc65010c7b7541a9551e002db12e9dae70"
+ "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/908247bc65010c7b7541a9551e002db12e9dae70",
- "reference": "908247bc65010c7b7541a9551e002db12e9dae70",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/87b233b00daf83fb70f40c9a28692be017ea7c6c",
+ "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c",
"shasum": ""
},
"require": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0",
"php": ">=5.4",
- "squizlabs/php_codesniffer": "^3.8.0 || 4.0.x-dev@dev"
+ "squizlabs/php_codesniffer": "^3.10.0 || 4.0.x-dev@dev"
},
"require-dev": {
"ext-filter": "*",
@@ -423,20 +453,20 @@
"type": "open_collective"
}
],
- "time": "2023-12-08T14:50:00+00:00"
+ "time": "2024-05-20T13:34:27+00:00"
},
{
"name": "sirbrillig/phpcs-changed",
- "version": "v2.11.4",
+ "version": "v2.11.5",
"source": {
"type": "git",
"url": "https://github.com/sirbrillig/phpcs-changed.git",
- "reference": "acc946731ec65053e49cb0d3185c8ffe74895f93"
+ "reference": "aaa144eb4f14697b6b06e3dcf74081b5a02f85f6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sirbrillig/phpcs-changed/zipball/acc946731ec65053e49cb0d3185c8ffe74895f93",
- "reference": "acc946731ec65053e49cb0d3185c8ffe74895f93",
+ "url": "https://api.github.com/repos/sirbrillig/phpcs-changed/zipball/aaa144eb4f14697b6b06e3dcf74081b5a02f85f6",
+ "reference": "aaa144eb4f14697b6b06e3dcf74081b5a02f85f6",
"shasum": ""
},
"require": {
@@ -475,22 +505,22 @@
"description": "Run phpcs on files, but only report warnings/errors from lines which were changed.",
"support": {
"issues": "https://github.com/sirbrillig/phpcs-changed/issues",
- "source": "https://github.com/sirbrillig/phpcs-changed/tree/v2.11.4"
+ "source": "https://github.com/sirbrillig/phpcs-changed/tree/v2.11.5"
},
- "time": "2023-09-29T21:27:51+00:00"
+ "time": "2024-05-23T20:01:41+00:00"
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.9.0",
+ "version": "3.10.1",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
- "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b"
+ "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b",
- "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/8f90f7a53ce271935282967f53d0894f8f1ff877",
+ "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877",
"shasum": ""
},
"require": {
@@ -557,7 +587,7 @@
"type": "open_collective"
}
],
- "time": "2024-02-16T15:06:51+00:00"
+ "time": "2024-05-22T21:24:41+00:00"
},
{
"name": "woocommerce/woocommerce-sniffs",
@@ -600,16 +630,16 @@
},
{
"name": "wp-coding-standards/wpcs",
- "version": "3.0.1",
+ "version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/WordPress/WordPress-Coding-Standards.git",
- "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1"
+ "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/b4caf9689f1a0e4a4c632679a44e638c1c67aff1",
- "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1",
+ "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/9333efcbff231f10dfd9c56bb7b65818b4733ca7",
+ "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7",
"shasum": ""
},
"require": {
@@ -618,16 +648,16 @@
"ext-tokenizer": "*",
"ext-xmlreader": "*",
"php": ">=5.4",
- "phpcsstandards/phpcsextra": "^1.1.0",
- "phpcsstandards/phpcsutils": "^1.0.8",
- "squizlabs/php_codesniffer": "^3.7.2"
+ "phpcsstandards/phpcsextra": "^1.2.1",
+ "phpcsstandards/phpcsutils": "^1.0.10",
+ "squizlabs/php_codesniffer": "^3.9.0"
},
"require-dev": {
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.0",
"phpcsstandards/phpcsdevtools": "^1.2.0",
- "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0"
},
"suggest": {
"ext-iconv": "For improved results",
@@ -658,11 +688,11 @@
},
"funding": [
{
- "url": "https://opencollective.com/thewpcc/contribute/wp-php-63406",
+ "url": "https://opencollective.com/php_codesniffer",
"type": "custom"
}
],
- "time": "2023-09-14T07:06:09+00:00"
+ "time": "2024-03-25T16:39:00+00:00"
}
],
"aliases": [],
diff --git a/plugins/woocommerce/bin/composer/phpunit/composer.lock b/plugins/woocommerce/bin/composer/phpunit/composer.lock
index b55266bb5dd..c438f265560 100644
--- a/plugins/woocommerce/bin/composer/phpunit/composer.lock
+++ b/plugins/woocommerce/bin/composer/phpunit/composer.lock
@@ -79,16 +79,16 @@
},
{
"name": "myclabs/deep-copy",
- "version": "1.11.1",
+ "version": "1.12.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
+ "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
- "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
+ "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
"shasum": ""
},
"require": {
@@ -96,11 +96,12 @@
},
"conflict": {
"doctrine/collections": "<1.6.8",
- "doctrine/common": "<2.13.3 || >=3,<3.2.2"
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
},
"require-dev": {
"doctrine/collections": "^1.6.8",
"doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
@@ -126,7 +127,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0"
},
"funding": [
{
@@ -134,20 +135,20 @@
"type": "tidelift"
}
],
- "time": "2023-03-08T13:26:56+00:00"
+ "time": "2024-06-12T14:39:25+00:00"
},
{
"name": "nikic/php-parser",
- "version": "v5.0.0",
+ "version": "v5.1.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc"
+ "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4a21235f7e56e713259a6f76bf4b5ea08502b9dc",
- "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1",
+ "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1",
"shasum": ""
},
"require": {
@@ -158,7 +159,7 @@
},
"require-dev": {
"ircmaxell/php-yacc": "^0.0.7",
- "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
+ "phpunit/phpunit": "^9.0"
},
"bin": [
"bin/php-parse"
@@ -190,26 +191,27 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.0"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0"
},
- "time": "2024-01-07T17:17:35+00:00"
+ "time": "2024-07-01T20:03:41+00:00"
},
{
"name": "phar-io/manifest",
- "version": "2.0.3",
+ "version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/phar-io/manifest.git",
- "reference": "97803eca37d319dfa7826cc2437fc020857acb53"
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53",
- "reference": "97803eca37d319dfa7826cc2437fc020857acb53",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
"shasum": ""
},
"require": {
"ext-dom": "*",
+ "ext-libxml": "*",
"ext-phar": "*",
"ext-xmlwriter": "*",
"phar-io/version": "^3.0.1",
@@ -250,9 +252,15 @@
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
"support": {
"issues": "https://github.com/phar-io/manifest/issues",
- "source": "https://github.com/phar-io/manifest/tree/2.0.3"
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
},
- "time": "2021-07-20T11:28:43+00:00"
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
},
{
"name": "phar-io/version",
@@ -307,16 +315,16 @@
},
{
"name": "phpunit/php-code-coverage",
- "version": "9.2.30",
+ "version": "9.2.31",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089"
+ "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089",
- "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965",
+ "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965",
"shasum": ""
},
"require": {
@@ -373,7 +381,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31"
},
"funding": [
{
@@ -381,7 +389,7 @@
"type": "github"
}
],
- "time": "2023-12-22T06:47:57+00:00"
+ "time": "2024-03-02T06:37:42+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -626,45 +634,45 @@
},
{
"name": "phpunit/phpunit",
- "version": "9.6.16",
+ "version": "9.6.20",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f"
+ "reference": "49d7820565836236411f5dc002d16dd689cde42f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3767b2c56ce02d01e3491046f33466a1ae60a37f",
- "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/49d7820565836236411f5dc002d16dd689cde42f",
+ "reference": "49d7820565836236411f5dc002d16dd689cde42f",
"shasum": ""
},
"require": {
- "doctrine/instantiator": "^1.3.1 || ^2",
+ "doctrine/instantiator": "^1.5.0 || ^2",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
- "myclabs/deep-copy": "^1.10.1",
- "phar-io/manifest": "^2.0.3",
- "phar-io/version": "^3.0.2",
+ "myclabs/deep-copy": "^1.12.0",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
"php": ">=7.3",
- "phpunit/php-code-coverage": "^9.2.28",
- "phpunit/php-file-iterator": "^3.0.5",
+ "phpunit/php-code-coverage": "^9.2.31",
+ "phpunit/php-file-iterator": "^3.0.6",
"phpunit/php-invoker": "^3.1.1",
- "phpunit/php-text-template": "^2.0.3",
- "phpunit/php-timer": "^5.0.2",
- "sebastian/cli-parser": "^1.0.1",
- "sebastian/code-unit": "^1.0.6",
+ "phpunit/php-text-template": "^2.0.4",
+ "phpunit/php-timer": "^5.0.3",
+ "sebastian/cli-parser": "^1.0.2",
+ "sebastian/code-unit": "^1.0.8",
"sebastian/comparator": "^4.0.8",
- "sebastian/diff": "^4.0.3",
- "sebastian/environment": "^5.1.3",
- "sebastian/exporter": "^4.0.5",
- "sebastian/global-state": "^5.0.1",
- "sebastian/object-enumerator": "^4.0.3",
- "sebastian/resource-operations": "^3.0.3",
- "sebastian/type": "^3.2",
+ "sebastian/diff": "^4.0.6",
+ "sebastian/environment": "^5.1.5",
+ "sebastian/exporter": "^4.0.6",
+ "sebastian/global-state": "^5.0.7",
+ "sebastian/object-enumerator": "^4.0.4",
+ "sebastian/resource-operations": "^3.0.4",
+ "sebastian/type": "^3.2.1",
"sebastian/version": "^3.0.2"
},
"suggest": {
@@ -709,7 +717,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.16"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.20"
},
"funding": [
{
@@ -725,20 +733,20 @@
"type": "tidelift"
}
],
- "time": "2024-01-19T07:03:14+00:00"
+ "time": "2024-07-10T11:45:39+00:00"
},
{
"name": "sebastian/cli-parser",
- "version": "1.0.1",
+ "version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/cli-parser.git",
- "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2"
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2",
- "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
"shasum": ""
},
"require": {
@@ -773,7 +781,7 @@
"homepage": "https://github.com/sebastianbergmann/cli-parser",
"support": {
"issues": "https://github.com/sebastianbergmann/cli-parser/issues",
- "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1"
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
},
"funding": [
{
@@ -781,7 +789,7 @@
"type": "github"
}
],
- "time": "2020-09-28T06:08:49+00:00"
+ "time": "2024-03-02T06:27:43+00:00"
},
{
"name": "sebastian/code-unit",
@@ -1027,16 +1035,16 @@
},
{
"name": "sebastian/diff",
- "version": "4.0.5",
+ "version": "4.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131"
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131",
- "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
"shasum": ""
},
"require": {
@@ -1081,7 +1089,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
- "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5"
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
},
"funding": [
{
@@ -1089,7 +1097,7 @@
"type": "github"
}
],
- "time": "2023-05-07T05:35:17+00:00"
+ "time": "2024-03-02T06:30:58+00:00"
},
{
"name": "sebastian/environment",
@@ -1156,16 +1164,16 @@
},
{
"name": "sebastian/exporter",
- "version": "4.0.5",
+ "version": "4.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d"
+ "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
- "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
+ "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
"shasum": ""
},
"require": {
@@ -1221,7 +1229,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
- "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5"
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
},
"funding": [
{
@@ -1229,20 +1237,20 @@
"type": "github"
}
],
- "time": "2022-09-14T06:03:37+00:00"
+ "time": "2024-03-02T06:33:00+00:00"
},
{
"name": "sebastian/global-state",
- "version": "5.0.6",
+ "version": "5.0.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "bde739e7565280bda77be70044ac1047bc007e34"
+ "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34",
- "reference": "bde739e7565280bda77be70044ac1047bc007e34",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
+ "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
"shasum": ""
},
"require": {
@@ -1285,7 +1293,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/global-state/issues",
- "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6"
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7"
},
"funding": [
{
@@ -1293,7 +1301,7 @@
"type": "github"
}
],
- "time": "2023-08-02T09:26:13+00:00"
+ "time": "2024-03-02T06:35:11+00:00"
},
{
"name": "sebastian/lines-of-code",
@@ -1529,16 +1537,16 @@
},
{
"name": "sebastian/resource-operations",
- "version": "3.0.3",
+ "version": "3.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/resource-operations.git",
- "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8"
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
- "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
"shasum": ""
},
"require": {
@@ -1550,7 +1558,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-main": "3.0-dev"
}
},
"autoload": {
@@ -1571,8 +1579,7 @@
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
"support": {
- "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
- "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3"
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
},
"funding": [
{
@@ -1580,7 +1587,7 @@
"type": "github"
}
],
- "time": "2020-09-28T06:45:17+00:00"
+ "time": "2024-03-14T16:00:52+00:00"
},
{
"name": "sebastian/type",
@@ -1693,16 +1700,16 @@
},
{
"name": "theseer/tokenizer",
- "version": "1.2.2",
+ "version": "1.2.3",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
- "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96"
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96",
- "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
"shasum": ""
},
"require": {
@@ -1731,7 +1738,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
- "source": "https://github.com/theseer/tokenizer/tree/1.2.2"
+ "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
},
"funding": [
{
@@ -1739,7 +1746,7 @@
"type": "github"
}
],
- "time": "2023-11-20T00:12:19+00:00"
+ "time": "2024-03-03T12:36:25+00:00"
}
],
"aliases": [],
diff --git a/plugins/woocommerce/bin/composer/wp/composer.lock b/plugins/woocommerce/bin/composer/wp/composer.lock
index 4de584d675e..e42bc2c8edf 100644
--- a/plugins/woocommerce/bin/composer/wp/composer.lock
+++ b/plugins/woocommerce/bin/composer/wp/composer.lock
@@ -67,16 +67,16 @@
},
{
"name": "gettext/gettext",
- "version": "v4.8.11",
+ "version": "v4.8.12",
"source": {
"type": "git",
"url": "https://github.com/php-gettext/Gettext.git",
- "reference": "b632aaf5e4579d0b2ae8bc61785e238bff4c5156"
+ "reference": "11af89ee6c087db3cf09ce2111a150bca5c46e12"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/b632aaf5e4579d0b2ae8bc61785e238bff4c5156",
- "reference": "b632aaf5e4579d0b2ae8bc61785e238bff4c5156",
+ "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/11af89ee6c087db3cf09ce2111a150bca5c46e12",
+ "reference": "11af89ee6c087db3cf09ce2111a150bca5c46e12",
"shasum": ""
},
"require": {
@@ -128,7 +128,7 @@
"support": {
"email": "oom@oscarotero.com",
"issues": "https://github.com/oscarotero/Gettext/issues",
- "source": "https://github.com/php-gettext/Gettext/tree/v4.8.11"
+ "source": "https://github.com/php-gettext/Gettext/tree/v4.8.12"
},
"funding": [
{
@@ -144,7 +144,7 @@
"type": "patreon"
}
],
- "time": "2023-08-14T15:15:05+00:00"
+ "time": "2024-05-18T10:25:07+00:00"
},
{
"name": "gettext/languages",
@@ -222,16 +222,16 @@
},
{
"name": "mck89/peast",
- "version": "v1.16.1",
+ "version": "v1.16.2",
"source": {
"type": "git",
"url": "https://github.com/mck89/peast.git",
- "reference": "f6e681062bb25c8dacbd30e079f4ad3fd890d7ad"
+ "reference": "2791b08ffcc1862fe18eef85675da3aa58c406fe"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mck89/peast/zipball/f6e681062bb25c8dacbd30e079f4ad3fd890d7ad",
- "reference": "f6e681062bb25c8dacbd30e079f4ad3fd890d7ad",
+ "url": "https://api.github.com/repos/mck89/peast/zipball/2791b08ffcc1862fe18eef85675da3aa58c406fe",
+ "reference": "2791b08ffcc1862fe18eef85675da3aa58c406fe",
"shasum": ""
},
"require": {
@@ -244,7 +244,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.16.1-dev"
+ "dev-master": "1.16.2-dev"
}
},
"autoload": {
@@ -265,9 +265,9 @@
"description": "Peast is PHP library that generates AST for JavaScript code",
"support": {
"issues": "https://github.com/mck89/peast/issues",
- "source": "https://github.com/mck89/peast/tree/v1.16.1"
+ "source": "https://github.com/mck89/peast/tree/v1.16.2"
},
- "time": "2024-02-14T08:15:19+00:00"
+ "time": "2024-03-05T09:16:03+00:00"
},
{
"name": "mustache/mustache",
@@ -373,16 +373,16 @@
},
{
"name": "wp-cli/i18n-command",
- "version": "v2.6.0",
+ "version": "2.6.1",
"source": {
"type": "git",
"url": "https://github.com/wp-cli/i18n-command.git",
- "reference": "ca7a44a1f8b09d533621b4006e739974212860ee"
+ "reference": "7538d684d4f06b0e10c8a0166ce4e6d9e1687aa1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/wp-cli/i18n-command/zipball/ca7a44a1f8b09d533621b4006e739974212860ee",
- "reference": "ca7a44a1f8b09d533621b4006e739974212860ee",
+ "url": "https://api.github.com/repos/wp-cli/i18n-command/zipball/7538d684d4f06b0e10c8a0166ce4e6d9e1687aa1",
+ "reference": "7538d684d4f06b0e10c8a0166ce4e6d9e1687aa1",
"shasum": ""
},
"require": {
@@ -436,9 +436,9 @@
"homepage": "https://github.com/wp-cli/i18n-command",
"support": {
"issues": "https://github.com/wp-cli/i18n-command/issues",
- "source": "https://github.com/wp-cli/i18n-command/tree/v2.6.0"
+ "source": "https://github.com/wp-cli/i18n-command/tree/2.6.1"
},
- "time": "2024-02-01T14:25:11+00:00"
+ "time": "2024-02-28T11:27:34+00:00"
},
{
"name": "wp-cli/mustangostang-spyc",
diff --git a/plugins/woocommerce/bin/lint-branch.sh b/plugins/woocommerce/bin/lint-branch.sh
index a079486ca07..562bd5c27a4 100644
--- a/plugins/woocommerce/bin/lint-branch.sh
+++ b/plugins/woocommerce/bin/lint-branch.sh
@@ -1,4 +1,5 @@
#!/usr/bin/env bash
+
# Lint branch
#
# Runs phpcs-changed, comparing the current branch to its "base" or "parent" branch.
@@ -9,22 +10,36 @@
# ./lint-branch.sh base-branch
baseBranch=${1:-"trunk"}
+redColoured='\033[0;31m'
# Lint changed php-files to match code style.
changedFiles=$(git diff $(git merge-base HEAD $baseBranch) --relative --name-only --diff-filter=d -- '*.php')
if [[ -n $changedFiles ]]; then
+ printf "Linting the following files with CodeSniffer:\n"
+ printf " %s\n" $changedFiles
+
composer exec phpcs-changed -- -s --git --git-base $baseBranch $changedFiles
+ if [[ $? != 0 ]]; then
+ printf "${redColoured}Unfortunately, CodeSniffer reported some violations which need to be addressed (see above).\n"
+ exit 1
+ else
+ printf "CodeSniffer reported no violations, well done!\n"
+ fi
fi
# Lint new (excl. renamed) php-files to contain strict types directive.
newFiles=$(git diff $(git merge-base HEAD $baseBranch) --relative --name-only --diff-filter=dmr -- '*.php')
if [[ -n $newFiles ]]; then
+ printf "Linting the new files for declaring strict types directive (https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.strict):\n"
+ printf " %s\n" $newFiles
+
passingFiles=$(find $newFiles -type f -exec grep -xl --regexp='declare(\s*strict_types\s*=\s*1\s*);' /dev/null {} +)
- violatingFiles=$(grep -vxf <(printf "%s\n" $passingFiles | sort) <(printf "%s\n" $newFiles | sort))
+ violatingFiles=$(grep -vxf <(printf "%s\n" $passingFiles | sort) <(printf "%s\n" $newFiles | sort) || echo '')
if [[ -n "$violatingFiles" ]]; then
- redColoured='\033[0;31m'
- printf "${redColoured}Following files are missing 'declare( strict_types = 1)' directive:\n"
- printf "${redColoured}%s\n" $violatingFiles
+ printf "${redColoured}Unfortunately, some files miss 'declare( strict_types = 1)' directive and need to be updated:\n"
+ printf "${redColoured} %s\n" $violatingFiles
exit 1
+ else
+ printf "Strict types directive linting reported no violations, well done!\n"
fi
fi
diff --git a/plugins/woocommerce/changelog/46201-product-collection-sync-with-current-query-doesnt-display-the-correct-products-in-the-specific-category-templates b/plugins/woocommerce/changelog/46201-product-collection-sync-with-current-query-doesnt-display-the-correct-products-in-the-specific-category-templates
new file mode 100644
index 00000000000..d97345f0b51
--- /dev/null
+++ b/plugins/woocommerce/changelog/46201-product-collection-sync-with-current-query-doesnt-display-the-correct-products-in-the-specific-category-templates
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Product Collection: fix the preview if used in Products by specific Category or Tag
diff --git a/plugins/woocommerce/changelog/47422-45145-woocommerce_new_order-triggers-on-checkout-visiting b/plugins/woocommerce/changelog/47422-45145-woocommerce_new_order-triggers-on-checkout-visiting
deleted file mode 100644
index 45a2a439c50..00000000000
--- a/plugins/woocommerce/changelog/47422-45145-woocommerce_new_order-triggers-on-checkout-visiting
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fixed "woocommerce_new_order" triggering on checkout blocks page visit.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/47444-add-disabled-save-settings-pages b/plugins/woocommerce/changelog/47444-add-disabled-save-settings-pages
deleted file mode 100644
index 3d3f7734e7f..00000000000
--- a/plugins/woocommerce/changelog/47444-add-disabled-save-settings-pages
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Update Settings to disable Save button unless modifications are made. #### Comment
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48128-update-wc-tasks-wccom-connection b/plugins/woocommerce/changelog/48128-update-wc-tasks-wccom-connection
deleted file mode 100644
index 505f1afc458..00000000000
--- a/plugins/woocommerce/changelog/48128-update-wc-tasks-wccom-connection
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-Update WC Tasks in the WC Home. Rename to WooCommerce marketplace, add new browse marketplace, remove connect to woocomerce.com from inbox
diff --git a/plugins/woocommerce/changelog/48140-tag-different-envs-in-e2e-tests b/plugins/woocommerce/changelog/48140-tag-different-envs-in-e2e-tests
deleted file mode 100644
index 0ff418b97df..00000000000
--- a/plugins/woocommerce/changelog/48140-tag-different-envs-in-e2e-tests
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-E2E tests: tag different envs in e2e test suite to run in workflows
diff --git a/plugins/woocommerce/changelog/48141-add-47962-product-collection-expose-experimental-registerproductcollection-for-3pds b/plugins/woocommerce/changelog/48141-add-47962-product-collection-expose-experimental-registerproductcollection-for-3pds
deleted file mode 100644
index f5c67073e5e..00000000000
--- a/plugins/woocommerce/changelog/48141-add-47962-product-collection-expose-experimental-registerproductcollection-for-3pds
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: add
-
-Expose __experimentalRegisterProductCollection in @woocommerce/blocks-registry Package
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48370-fix-shipping-focus-lost-48104 b/plugins/woocommerce/changelog/48370-fix-shipping-focus-lost-48104
deleted file mode 100644
index 3f6bf4f7d5d..00000000000
--- a/plugins/woocommerce/changelog/48370-fix-shipping-focus-lost-48104
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Accessibility: Prevent shipping losing focus when making selections during checkout.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48371-frosso-patch-1 b/plugins/woocommerce/changelog/48371-frosso-patch-1
new file mode 100644
index 00000000000..8b1a47a28e3
--- /dev/null
+++ b/plugins/woocommerce/changelog/48371-frosso-patch-1
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+feat: add `aria-required` attributes to WC form fields
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48382-refactor-btc-initialization b/plugins/woocommerce/changelog/48382-refactor-btc-initialization
deleted file mode 100644
index cc821f00cf0..00000000000
--- a/plugins/woocommerce/changelog/48382-refactor-btc-initialization
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-
-Initialize BlockTemplatesController for block themes only.
diff --git a/plugins/woocommerce/changelog/48384-fix-mini-cart-zoom b/plugins/woocommerce/changelog/48384-fix-mini-cart-zoom
deleted file mode 100644
index a735f90657b..00000000000
--- a/plugins/woocommerce/changelog/48384-fix-mini-cart-zoom
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fix the mini cart items not being visible when zoomed in
diff --git a/plugins/woocommerce/changelog/48445-update-get-tax-line-label b/plugins/woocommerce/changelog/48445-update-get-tax-line-label
deleted file mode 100644
index c56b54da39a..00000000000
--- a/plugins/woocommerce/changelog/48445-update-get-tax-line-label
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-
-Get tax line label instead of name in StoreAPI Order endpoint.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48487-update-store-alert-styles b/plugins/woocommerce/changelog/48487-update-store-alert-styles
deleted file mode 100644
index 2d795c21ecb..00000000000
--- a/plugins/woocommerce/changelog/48487-update-store-alert-styles
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Update Store Alert widths to match main body
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48488-48459-coupon-email-restriction-fix b/plugins/woocommerce/changelog/48488-48459-coupon-email-restriction-fix
deleted file mode 100644
index 3a5515659bd..00000000000
--- a/plugins/woocommerce/changelog/48488-48459-coupon-email-restriction-fix
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fixed a bug causing account email not to be taken in consideration for coupon validation when a customer has a different billing email set.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48492-45835-migrate-patterns-ptk b/plugins/woocommerce/changelog/48492-45835-migrate-patterns-ptk
deleted file mode 100644
index e18eda3aa8b..00000000000
--- a/plugins/woocommerce/changelog/48492-45835-migrate-patterns-ptk
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Fetch patterns from the WooCommerce PTK source site.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48513-add-47963-product-collection-add-validation-for-registerproductcollection-arguments b/plugins/woocommerce/changelog/48513-add-47963-product-collection-add-validation-for-registerproductcollection-arguments
deleted file mode 100644
index ec8d73c14db..00000000000
--- a/plugins/woocommerce/changelog/48513-add-47963-product-collection-add-validation-for-registerproductcollection-arguments
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: add
-
-Add validation for `__experimentalRegisterProductCollection` arguments
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48545-add-product-count-display-setting b/plugins/woocommerce/changelog/48545-add-product-count-display-setting
deleted file mode 100644
index ea693808508..00000000000
--- a/plugins/woocommerce/changelog/48545-add-product-count-display-setting
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-Adds setting to control the visibility of product count in Mini cart block
diff --git a/plugins/woocommerce/changelog/48618-48601-filter-deps-ptk-patterns b/plugins/woocommerce/changelog/48618-48601-filter-deps-ptk-patterns
deleted file mode 100644
index caa07749b98..00000000000
--- a/plugins/woocommerce/changelog/48618-48601-filter-deps-ptk-patterns
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Filter out patterns with external dependencies.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48635-fix-46984-add-correct-icon-to-product-filters-wrapper-block b/plugins/woocommerce/changelog/48635-fix-46984-add-correct-icon-to-product-filters-wrapper-block
deleted file mode 100644
index a23f50a48b0..00000000000
--- a/plugins/woocommerce/changelog/48635-fix-46984-add-correct-icon-to-product-filters-wrapper-block
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: The block is currently on experimental state, so the changes done in this PR won't be available for the users.
-
diff --git a/plugins/woocommerce/changelog/48638-feat-48637-implement-dynamic-block-icon-and-title b/plugins/woocommerce/changelog/48638-feat-48637-implement-dynamic-block-icon-and-title
deleted file mode 100644
index 04afcbf480e..00000000000
--- a/plugins/woocommerce/changelog/48638-feat-48637-implement-dynamic-block-icon-and-title
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: This PR does not require a changelog as the changes are behind an experimental flag and will not affect users.
-
diff --git a/plugins/woocommerce/changelog/48659-product-filters-overlay-template-part-e2e b/plugins/woocommerce/changelog/48659-product-filters-overlay-template-part-e2e
deleted file mode 100644
index f88a4745513..00000000000
--- a/plugins/woocommerce/changelog/48659-product-filters-overlay-template-part-e2e
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: Just fixing an e2e test.
-
diff --git a/plugins/woocommerce/changelog/48662-48551-cys-update-block-toolbar-position b/plugins/woocommerce/changelog/48662-48551-cys-update-block-toolbar-position
deleted file mode 100644
index 240ca25acff..00000000000
--- a/plugins/woocommerce/changelog/48662-48551-cys-update-block-toolbar-position
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS: Update Block Toolbar Position
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48665-48381-pattern-categories-copy b/plugins/woocommerce/changelog/48665-48381-pattern-categories-copy
deleted file mode 100644
index c4e948cc368..00000000000
--- a/plugins/woocommerce/changelog/48665-48381-pattern-categories-copy
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Update pattern categories and its descriptions.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48668-47464-cys-full-composability-ensure-adiscreetbadge-is-displayed-informing-how-many-patterns-have-been-inserted-from-each-category b/plugins/woocommerce/changelog/48668-47464-cys-full-composability-ensure-adiscreetbadge-is-displayed-informing-how-many-patterns-have-been-inserted-from-each-category
deleted file mode 100644
index e2f109552fd..00000000000
--- a/plugins/woocommerce/changelog/48668-47464-cys-full-composability-ensure-adiscreetbadge-is-displayed-informing-how-many-patterns-have-been-inserted-from-each-category
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: add
-
-CYS: add badge that informs how many patterns have been inserted from each category.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48671-48441-fix-long-title-length b/plugins/woocommerce/changelog/48671-48441-fix-long-title-length
deleted file mode 100644
index 782f1a622f4..00000000000
--- a/plugins/woocommerce/changelog/48671-48441-fix-long-title-length
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-CYS - Remove the site title block length from the "Large Header" and the "Centered Menu Header".
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48674-register-testimonials-as-reviews b/plugins/woocommerce/changelog/48674-register-testimonials-as-reviews
deleted file mode 100644
index d482e03f73b..00000000000
--- a/plugins/woocommerce/changelog/48674-register-testimonials-as-reviews
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Register PTK "Testimonials" patterns as "Reviews"
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48699-tweak-dont-recommend-wcservices-in-tax-task-with-wcship-or-wctax-active b/plugins/woocommerce/changelog/48699-tweak-dont-recommend-wcservices-in-tax-task-with-wcship-or-wctax-active
deleted file mode 100644
index 10b5dc18680..00000000000
--- a/plugins/woocommerce/changelog/48699-tweak-dont-recommend-wcservices-in-tax-task-with-wcship-or-wctax-active
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: If either the upcoming WooCommerce Shipping or WooCommerce Tax extension is active, don't suggest WooCommerce Shipping & Tax in the "Collect sales tax" Home task. This only adjusts the extension recommendation merchants receive and does not affect any other functionality.
-
diff --git a/plugins/woocommerce/changelog/48701-tweak-dont-recommend-wcservices-in-shipping-settings-if-wcship-or-wctax-is-active b/plugins/woocommerce/changelog/48701-tweak-dont-recommend-wcservices-in-shipping-settings-if-wcship-or-wctax-is-active
deleted file mode 100644
index 788dd1a7219..00000000000
--- a/plugins/woocommerce/changelog/48701-tweak-dont-recommend-wcservices-in-shipping-settings-if-wcship-or-wctax-is-active
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: If either the upcoming WooCommerce Shipping or WooCommerce Tax extension is active, don't suggest WooCommerce Shipping & Tax on the WC > Settings > Shipping page. This only adjusts the extension recommendation merchants receive and does not affect any other functionality.
-
diff --git a/plugins/woocommerce/changelog/48703-tweak-dont-display-wcservices-help-if-wcship-or-wctax-is-active b/plugins/woocommerce/changelog/48703-tweak-dont-display-wcservices-help-if-wcship-or-wctax-is-active
deleted file mode 100644
index 857914c18ff..00000000000
--- a/plugins/woocommerce/changelog/48703-tweak-dont-display-wcservices-help-if-wcship-or-wctax-is-active
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: If either the upcoming WooCommerce Shipping or WooCommerce Tax extension is active, don't display information about setting up WooCommerce Shipping & Tax in the task steppers for "Collect sales tax" and "Select your shipping options". The changes in this PR only adjust the help items merchants see.
-
diff --git a/plugins/woocommerce/changelog/48704-tweak-dont-recommend-wcservices-on-edit-order-page-with-wcship-or-wctax-active b/plugins/woocommerce/changelog/48704-tweak-dont-recommend-wcservices-on-edit-order-page-with-wcship-or-wctax-active
deleted file mode 100644
index b16858257fc..00000000000
--- a/plugins/woocommerce/changelog/48704-tweak-dont-recommend-wcservices-on-edit-order-page-with-wcship-or-wctax-active
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: If either the upcoming WooCommerce Shipping or WooCommerce Tax extension is active, don't display a banner recommending WooCommerce Shipping & Tax on the "Edit order" page. The changes in this PR only remove the banner recommending WCS&T which, if installed, would lead to a notice about incompatibility.
-
diff --git a/plugins/woocommerce/changelog/48705-update-dont-recommend-wcservices-in-default-extension-recommendations-with-wcship-or-wctax-active b/plugins/woocommerce/changelog/48705-update-dont-recommend-wcservices-in-default-extension-recommendations-with-wcship-or-wctax-active
deleted file mode 100644
index 48b15c29407..00000000000
--- a/plugins/woocommerce/changelog/48705-update-dont-recommend-wcservices-in-default-extension-recommendations-with-wcship-or-wctax-active
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: If either the upcoming WooCommerce Shipping or WooCommerce Tax extension is active, don't recommend WooCommerce Shipping & Tax in the onboarding wizards and the "Select your shipping options" task. Allowing WCS&T to be installed alongside WC Shipping or WC Tax would lead to a notice about incompatibility which is something this PR aims to avoid.
-
diff --git a/plugins/woocommerce/changelog/48712-48711-cys-the-added-patterns-are-wrapped-twice-by-group-blocks b/plugins/woocommerce/changelog/48712-48711-cys-the-added-patterns-are-wrapped-twice-by-group-blocks
deleted file mode 100644
index bca06f1c0c1..00000000000
--- a/plugins/woocommerce/changelog/48712-48711-cys-the-added-patterns-are-wrapped-twice-by-group-blocks
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-CYS: fix pattern wrapped twice by group blocks
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48714-48713-cys-button-to-resize-the-image-is-visible b/plugins/woocommerce/changelog/48714-48713-cys-button-to-resize-the-image-is-visible
deleted file mode 100644
index 80e203ce5ee..00000000000
--- a/plugins/woocommerce/changelog/48714-48713-cys-button-to-resize-the-image-is-visible
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-CYS: hide button to resize the image
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48717-fix-48695-fix-confusing-messages-prompting-switch-to-classic-templates b/plugins/woocommerce/changelog/48717-fix-48695-fix-confusing-messages-prompting-switch-to-classic-templates
deleted file mode 100644
index 865b845d5dd..00000000000
--- a/plugins/woocommerce/changelog/48717-fix-48695-fix-confusing-messages-prompting-switch-to-classic-templates
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fix confusing messages prompting switch to classic templates
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48719-fix-ptk-unit-test b/plugins/woocommerce/changelog/48719-fix-ptk-unit-test
deleted file mode 100644
index 2b57e9e0548..00000000000
--- a/plugins/woocommerce/changelog/48719-fix-ptk-unit-test
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-CYS - Fix the "test_fetch_patterns_should_register_testimonials_category_as_reviews" tests.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48722-48682-cys-when-the-user-clicks-on-the-no-block-placeholder-the-tooltip-is-rendered b/plugins/woocommerce/changelog/48722-48682-cys-when-the-user-clicks-on-the-no-block-placeholder-the-tooltip-is-rendered
deleted file mode 100644
index d318463c5f7..00000000000
--- a/plugins/woocommerce/changelog/48722-48682-cys-when-the-user-clicks-on-the-no-block-placeholder-the-tooltip-is-rendered
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-CYS: fix logic to disable click on the no block placeholder
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48750-47402-cys-patterns-migration-ensure-a-handful-of-pre-selected-patterns-are-still-kept-within-the-woocommerce-plugin b/plugins/woocommerce/changelog/48750-47402-cys-patterns-migration-ensure-a-handful-of-pre-selected-patterns-are-still-kept-within-the-woocommerce-plugin
deleted file mode 100644
index 9ae62782bbe..00000000000
--- a/plugins/woocommerce/changelog/48750-47402-cys-patterns-migration-ensure-a-handful-of-pre-selected-patterns-are-still-kept-within-the-woocommerce-plugin
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS: Remove not necessary patterns.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48752-fix-read-only-assembler b/plugins/woocommerce/changelog/48752-fix-read-only-assembler
deleted file mode 100644
index a0dcd2a18c6..00000000000
--- a/plugins/woocommerce/changelog/48752-fix-read-only-assembler
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-CYS: Disable readonly mode only when full composability feature flag is enabled.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48756-fix-tags-overlapping-divider-line-issue b/plugins/woocommerce/changelog/48756-fix-tags-overlapping-divider-line-issue
deleted file mode 100644
index 2c8a23bf076..00000000000
--- a/plugins/woocommerce/changelog/48756-fix-tags-overlapping-divider-line-issue
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Issue fixed where tags are overlapping divider line in "Filter by product category".
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48762-update-disable-toggle-by-default-and-secondary-enhancements b/plugins/woocommerce/changelog/48762-update-disable-toggle-by-default-and-secondary-enhancements
deleted file mode 100644
index 2a7c410ecc8..00000000000
--- a/plugins/woocommerce/changelog/48762-update-disable-toggle-by-default-and-secondary-enhancements
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: enhancement
-
-Made the "return to cart" link (in the checkout block) hidden by default.
diff --git a/plugins/woocommerce/changelog/48765-48669-cys-in-some-cases-the-border-of-the-footerheader-is-violet b/plugins/woocommerce/changelog/48765-48669-cys-in-some-cases-the-border-of-the-footerheader-is-violet
deleted file mode 100644
index 801cf3d9592..00000000000
--- a/plugins/woocommerce/changelog/48765-48669-cys-in-some-cases-the-border-of-the-footerheader-is-violet
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS: when the footer/header is clicked, the border color is blue.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48767-48728-cys-pop-up-effect-opt-in-tracking-section-notice b/plugins/woocommerce/changelog/48767-48728-cys-pop-up-effect-opt-in-tracking-section-notice
deleted file mode 100644
index f364fba2252..00000000000
--- a/plugins/woocommerce/changelog/48767-48728-cys-pop-up-effect-opt-in-tracking-section-notice
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-CYS: fix flickering effect.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48782-update-add-comment-for-regression-tests b/plugins/woocommerce/changelog/48782-update-add-comment-for-regression-tests
deleted file mode 100644
index a567dcef44c..00000000000
--- a/plugins/woocommerce/changelog/48782-update-add-comment-for-regression-tests
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: This just adds a comment to the regression test to make it easier to reference in the future.
-
diff --git a/plugins/woocommerce/changelog/48795-fix-47645-dont-show-the-badge-when-the-inner-blocks-are-selected b/plugins/woocommerce/changelog/48795-fix-47645-dont-show-the-badge-when-the-inner-blocks-are-selected
deleted file mode 100644
index 8577919e5b9..00000000000
--- a/plugins/woocommerce/changelog/48795-fix-47645-dont-show-the-badge-when-the-inner-blocks-are-selected
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-Fix: Show preview label only when Product Collection block is selected
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48799-48787-cys-as-soon-as-the-user-moves-away-from-the-pattern-the-block-toolbar-should-disappear b/plugins/woocommerce/changelog/48799-48787-cys-as-soon-as-the-user-moves-away-from-the-pattern-the-block-toolbar-should-disappear
deleted file mode 100644
index 139ecd0c1fa..00000000000
--- a/plugins/woocommerce/changelog/48799-48787-cys-as-soon-as-the-user-moves-away-from-the-pattern-the-block-toolbar-should-disappear
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS: Improve Block Toolbar logic.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48802-48786-cys-no-highligh-the-pattern-when-it-is-added b/plugins/woocommerce/changelog/48802-48786-cys-no-highligh-the-pattern-when-it-is-added
deleted file mode 100644
index 0bf2102a769..00000000000
--- a/plugins/woocommerce/changelog/48802-48786-cys-no-highligh-the-pattern-when-it-is-added
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS: no highlight the pattern when it is added.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48839-add-cys-default-pattern b/plugins/woocommerce/changelog/48839-add-cys-default-pattern
deleted file mode 100644
index 54816ee1dd1..00000000000
--- a/plugins/woocommerce/changelog/48839-add-cys-default-pattern
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: add
-
-CYS: Add default patterns.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48858-update-48797-use-createRoot-instead-of-render b/plugins/woocommerce/changelog/48843-update-48796-use-createRoot-instead-of-render
similarity index 100%
rename from plugins/woocommerce/changelog/48858-update-48797-use-createRoot-instead-of-render
rename to plugins/woocommerce/changelog/48843-update-48796-use-createRoot-instead-of-render
diff --git a/plugins/woocommerce/changelog/48856-fix-47646-fix-the-preview-badges-corner-radius b/plugins/woocommerce/changelog/48856-fix-47646-fix-the-preview-badges-corner-radius
deleted file mode 100644
index 9ae34849b7c..00000000000
--- a/plugins/woocommerce/changelog/48856-fix-47646-fix-the-preview-badges-corner-radius
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Product Collection: Fix the Preview badge's corner radius
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48867-product-collection-sync-with-current-query-should-only-only-in-archive-context-and-not-other-places b/plugins/woocommerce/changelog/48867-product-collection-sync-with-current-query-should-only-only-in-archive-context-and-not-other-places
deleted file mode 100644
index c7ba018a187..00000000000
--- a/plugins/woocommerce/changelog/48867-product-collection-sync-with-current-query-should-only-only-in-archive-context-and-not-other-places
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Product Collection: Show "Sync with current query" option only in archive templates where it makes sense
diff --git a/plugins/woocommerce/changelog/48882-48784-cys-update-design-your-homepage-sidebar-copy b/plugins/woocommerce/changelog/48882-48784-cys-update-design-your-homepage-sidebar-copy
deleted file mode 100644
index 17898f264c2..00000000000
--- a/plugins/woocommerce/changelog/48882-48784-cys-update-design-your-homepage-sidebar-copy
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS: Update sidebar homepage copy
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48889-48888-cys-block-toolbar-make-the-entire-shuffle-section-clickable b/plugins/woocommerce/changelog/48889-48888-cys-block-toolbar-make-the-entire-shuffle-section-clickable
deleted file mode 100644
index 80e444f4de5..00000000000
--- a/plugins/woocommerce/changelog/48889-48888-cys-block-toolbar-make-the-entire-shuffle-section-clickable
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: enhancement
-
-CYS: make the entire shuffle section clickable.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48937-update-perf-test-my-account-orders-id-boundary b/plugins/woocommerce/changelog/48937-update-perf-test-my-account-orders-id-boundary
new file mode 100644
index 00000000000..9008e91250a
--- /dev/null
+++ b/plugins/woocommerce/changelog/48937-update-perf-test-my-account-orders-id-boundary
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+Comment: This is an update to a performance test id boundary and does not require a changelog
+
diff --git a/plugins/woocommerce/changelog/48941-fix-cys-remove-animation b/plugins/woocommerce/changelog/48941-fix-cys-remove-animation
deleted file mode 100644
index f1a282c38c0..00000000000
--- a/plugins/woocommerce/changelog/48941-fix-cys-remove-animation
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: enhancement
-
-CYS: Remove iframe animation
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48969-dev-dont-warn-aria-missing b/plugins/woocommerce/changelog/48969-dev-dont-warn-aria-missing
new file mode 100644
index 00000000000..d1387dbf02b
--- /dev/null
+++ b/plugins/woocommerce/changelog/48969-dev-dont-warn-aria-missing
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Avoid PHP warnings if `add-to-cart.php` template does not pass `aria-describedby_text`
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48979-48793-add-icon-customer-account-block b/plugins/woocommerce/changelog/48979-48793-add-icon-customer-account-block
deleted file mode 100644
index a37c4995640..00000000000
--- a/plugins/woocommerce/changelog/48979-48793-add-icon-customer-account-block
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-Add a new icon style to the "Customer Account" block.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48983-48947-fix-intro-page b/plugins/woocommerce/changelog/48983-48947-fix-intro-page
deleted file mode 100644
index 7bc08b1fbad..00000000000
--- a/plugins/woocommerce/changelog/48983-48947-fix-intro-page
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Improve the designs of the Intro page bottom cards.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48987-update-cys-fiverr-logo-maker-cta-copy b/plugins/woocommerce/changelog/48987-update-cys-fiverr-logo-maker-cta-copy
deleted file mode 100644
index 6f22aa3d30c..00000000000
--- a/plugins/woocommerce/changelog/48987-update-cys-fiverr-logo-maker-cta-copy
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS: Update verbiage in the CTA to our Fiverr Logo Maker landing page.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48989-sku-fix-failing-test b/plugins/woocommerce/changelog/48989-sku-fix-failing-test
deleted file mode 100644
index 35377024795..00000000000
--- a/plugins/woocommerce/changelog/48989-sku-fix-failing-test
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: No changelog entry is required, as this just changes a unit test to workaround an issue.
-
diff --git a/plugins/woocommerce/changelog/49005-fix-open-intro-section b/plugins/woocommerce/changelog/49005-fix-open-intro-section
deleted file mode 100644
index a3f3b40ac7e..00000000000
--- a/plugins/woocommerce/changelog/49005-fix-open-intro-section
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-CYS: open Intro panel when user clicks on Design your homepage
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49006-fix-32202-fatal-error-when-parent-order-doesnt-exist b/plugins/woocommerce/changelog/49006-fix-32202-fatal-error-when-parent-order-doesnt-exist
deleted file mode 100644
index 0b3f4bb7d49..00000000000
--- a/plugins/woocommerce/changelog/49006-fix-32202-fatal-error-when-parent-order-doesnt-exist
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fix fatal error in order reports when parent order doesn't exist
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49007-48603-fetch-dotcom-patterns b/plugins/woocommerce/changelog/49007-48603-fetch-dotcom-patterns
deleted file mode 100644
index cf03238760f..00000000000
--- a/plugins/woocommerce/changelog/49007-48603-fetch-dotcom-patterns
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Fetch patterns from the private dotcom patterns category instead of from the default source site.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49018-fix-block-e2e-readme b/plugins/woocommerce/changelog/49018-fix-block-e2e-readme
deleted file mode 100644
index f32a4236bbc..00000000000
--- a/plugins/woocommerce/changelog/49018-fix-block-e2e-readme
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Fix broken syntax in e2e-guidelines.md.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49027-remove-ie-styles b/plugins/woocommerce/changelog/49027-remove-ie-styles
deleted file mode 100644
index d68c3613e40..00000000000
--- a/plugins/woocommerce/changelog/49027-remove-ie-styles
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-
-Remove unneeded IE styling as IE is no longer supported
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49028-49024-cys-block-toolbar-isnt-in-the-right-position-when-the-user-resize-the-viewport b/plugins/woocommerce/changelog/49028-49024-cys-block-toolbar-isnt-in-the-right-position-when-the-user-resize-the-viewport
deleted file mode 100644
index 95fc205660f..00000000000
--- a/plugins/woocommerce/changelog/49028-49024-cys-block-toolbar-isnt-in-the-right-position-when-the-user-resize-the-viewport
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-CYS: fix toolbar position after the site preview resizes
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49030-48683-cys-improve-copyux-when-the-user-cant-save-due-block-placeholder b/plugins/woocommerce/changelog/49030-48683-cys-improve-copyux-when-the-user-cant-save-due-block-placeholder
deleted file mode 100644
index 757ec671433..00000000000
--- a/plugins/woocommerce/changelog/49030-48683-cys-improve-copyux-when-the-user-cant-save-due-block-placeholder
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-CYS: improve copy no blocks placeholder.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49037-update-e2e-wp-version b/plugins/woocommerce/changelog/49037-update-e2e-wp-version
deleted file mode 100644
index 807ff491e3e..00000000000
--- a/plugins/woocommerce/changelog/49037-update-e2e-wp-version
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: This is a monorepo-facing change that updates E2E tests in our CI.
-
diff --git a/plugins/woocommerce/changelog/49041-48680-cys-block-list-patterns-woo-patterns-preview-arent-rendered-correctly b/plugins/woocommerce/changelog/49041-48680-cys-block-list-patterns-woo-patterns-preview-arent-rendered-correctly
deleted file mode 100644
index 11339c6d825..00000000000
--- a/plugins/woocommerce/changelog/49041-48680-cys-block-list-patterns-woo-patterns-preview-arent-rendered-correctly
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-CYS: Fix pattern rendering issues
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49075-fix-LYS-content-container-height b/plugins/woocommerce/changelog/49075-fix-LYS-content-container-height
deleted file mode 100644
index d8ab5f80da1..00000000000
--- a/plugins/woocommerce/changelog/49075-fix-LYS-content-container-height
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: Fixes an unreleased CSS bug
-
diff --git a/plugins/woocommerce/changelog/49082-fix-intro-default-pattern-image b/plugins/woocommerce/changelog/49082-fix-intro-default-pattern-image
deleted file mode 100644
index d40c663bb26..00000000000
--- a/plugins/woocommerce/changelog/49082-fix-intro-default-pattern-image
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-CYS: fix the default intro pattern.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49096-fix-remove-experimental-layout-from-product-template b/plugins/woocommerce/changelog/49096-fix-remove-experimental-layout-from-product-template
deleted file mode 100644
index 51455e535fd..00000000000
--- a/plugins/woocommerce/changelog/49096-fix-remove-experimental-layout-from-product-template
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-Product Collection: Fix alignment of the first item in Grid layout for WP 6.6
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49101-pattern-reviews b/plugins/woocommerce/changelog/49101-pattern-reviews
deleted file mode 100644
index aa6a37ef54b..00000000000
--- a/plugins/woocommerce/changelog/49101-pattern-reviews
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Remove non-default patterns and register them from the PTK. Update margins.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49133-49110-fix-account-icon-headers b/plugins/woocommerce/changelog/49133-49110-fix-account-icon-headers
deleted file mode 100644
index ee744f69f24..00000000000
--- a/plugins/woocommerce/changelog/49133-49110-fix-account-icon-headers
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS: Update icon used by the "Customer account" block into header patterns
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49140-add-dotcom-reviews-patterns b/plugins/woocommerce/changelog/49140-add-dotcom-reviews-patterns
deleted file mode 100644
index 8a72c6e91c3..00000000000
--- a/plugins/woocommerce/changelog/49140-add-dotcom-reviews-patterns
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Include the dotcom patterns from the "Reviews" category.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49151-cys-patterns-bold-titles b/plugins/woocommerce/changelog/49151-cys-patterns-bold-titles
deleted file mode 100644
index 831a77e6648..00000000000
--- a/plugins/woocommerce/changelog/49151-cys-patterns-bold-titles
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Make some titles bold on CYS patterns.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49153-49128-cys-shuffle-feature-select-patterns-from-another-category b/plugins/woocommerce/changelog/49153-49128-cys-shuffle-feature-select-patterns-from-another-category
deleted file mode 100644
index cd8a541d50a..00000000000
--- a/plugins/woocommerce/changelog/49153-49128-cys-shuffle-feature-select-patterns-from-another-category
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-CYS: fix shuffle feature logic.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49154-add-missing-core-patterns b/plugins/woocommerce/changelog/49154-add-missing-core-patterns
deleted file mode 100644
index 46a8ce02d69..00000000000
--- a/plugins/woocommerce/changelog/49154-add-missing-core-patterns
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Add missing patterns to their categories for the assembler
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49159-fix-match-admin-color-schema b/plugins/woocommerce/changelog/49159-fix-match-admin-color-schema
deleted file mode 100644
index d37cfdb4c94..00000000000
--- a/plugins/woocommerce/changelog/49159-fix-match-admin-color-schema
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-CYS: fix: Assembler follows admin color schema.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49170-fix-49169 b/plugins/woocommerce/changelog/49170-fix-49169
deleted file mode 100644
index 232e12c268b..00000000000
--- a/plugins/woocommerce/changelog/49170-fix-49169
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Prevent product editor styles loading on non wc-admin pages
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49181-fix-color-button-pattern b/plugins/woocommerce/changelog/49181-fix-color-button-pattern
deleted file mode 100644
index b6c73c059e7..00000000000
--- a/plugins/woocommerce/changelog/49181-fix-color-button-pattern
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Fix dark patterns buttons color.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49196-fix-spacing b/plugins/woocommerce/changelog/49196-fix-spacing
deleted file mode 100644
index e95ee6e77a7..00000000000
--- a/plugins/woocommerce/changelog/49196-fix-spacing
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Improve margins for CYS core patterns.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49206-fix-hover-border b/plugins/woocommerce/changelog/49206-fix-hover-border
deleted file mode 100644
index 0635e4825fe..00000000000
--- a/plugins/woocommerce/changelog/49206-fix-hover-border
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Fix the pattern preview border color on hover and for inserted patterns.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49231-fix-logo-title-font-size b/plugins/woocommerce/changelog/49231-fix-logo-title-font-size
deleted file mode 100644
index 43549ad6e02..00000000000
--- a/plugins/woocommerce/changelog/49231-fix-logo-title-font-size
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Fix the font size of the "DON'T HAVE A LOGO YET?" title.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49232-fix-css-issues-cys b/plugins/woocommerce/changelog/49232-fix-css-issues-cys
deleted file mode 100644
index 93c40cb78a6..00000000000
--- a/plugins/woocommerce/changelog/49232-fix-css-issues-cys
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-CYS - Fix CSS spacing issues in the assembler.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49244-fix-37940-remove-simple-products-from-variation-report b/plugins/woocommerce/changelog/49244-fix-37940-remove-simple-products-from-variation-report
deleted file mode 100644
index b838c1ee429..00000000000
--- a/plugins/woocommerce/changelog/49244-fix-37940-remove-simple-products-from-variation-report
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-Exclude simple products from variations reports by default.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49304-update-express-checkout-layout b/plugins/woocommerce/changelog/49304-update-express-checkout-layout
new file mode 100644
index 00000000000..bfeb46cfab9
--- /dev/null
+++ b/plugins/woocommerce/changelog/49304-update-express-checkout-layout
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fixes a bug where some express payment buttons weren't being rendered correctly
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49737-fix_add_login_in_reset_pass_emails b/plugins/woocommerce/changelog/49737-fix_add_login_in_reset_pass_emails
new file mode 100644
index 00000000000..fd884039ac5
--- /dev/null
+++ b/plugins/woocommerce/changelog/49737-fix_add_login_in_reset_pass_emails
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Add username in email reset-password link
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49748-49589-new-cys-homepage-tests b/plugins/woocommerce/changelog/49748-49589-new-cys-homepage-tests
new file mode 100644
index 00000000000..acceaa5c4c4
--- /dev/null
+++ b/plugins/woocommerce/changelog/49748-49589-new-cys-homepage-tests
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+CYS - Add tests for the Full Composability feature.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49758-update-49210-restrict-wccomhelper-to-admin b/plugins/woocommerce/changelog/49758-update-49210-restrict-wccomhelper-to-admin
new file mode 100644
index 00000000000..f4767ee18cd
--- /dev/null
+++ b/plugins/woocommerce/changelog/49758-update-49210-restrict-wccomhelper-to-admin
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Ensure `wccomHelper` data is only loaded on the Extensions page where it's needed.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/49961-update-server-caching-link b/plugins/woocommerce/changelog/49961-update-server-caching-link
new file mode 100644
index 00000000000..0f9b21daef7
--- /dev/null
+++ b/plugins/woocommerce/changelog/49961-update-server-caching-link
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+Comment: This PR only updates an in-product link to the docs. The doc content is the same and has only been moved. No changelog entry needed IMO
+
diff --git a/plugins/woocommerce/changelog/49970-fix-cys-skip-test-wordpress-6-5 b/plugins/woocommerce/changelog/49970-fix-cys-skip-test-wordpress-6-5
new file mode 100644
index 00000000000..b43e5312c34
--- /dev/null
+++ b/plugins/woocommerce/changelog/49970-fix-cys-skip-test-wordpress-6-5
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+Comment: Skip PTK is down test on WordPress 6.5
+
diff --git a/plugins/woocommerce/changelog/49999-feat-49998-replace-product-filters-block-with-template-part-in-overlay-template-part b/plugins/woocommerce/changelog/49999-feat-49998-replace-product-filters-block-with-template-part-in-overlay-template-part
new file mode 100644
index 00000000000..3db8fb453d6
--- /dev/null
+++ b/plugins/woocommerce/changelog/49999-feat-49998-replace-product-filters-block-with-template-part-in-overlay-template-part
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+Comment: Replace the Product Filters block within the Product Filters Overlay template part with the Product Filters template part. This functionality is experimental and not yet available.
+
diff --git a/plugins/woocommerce/changelog/50016-skip-cys-test-wordpress-version b/plugins/woocommerce/changelog/50016-skip-cys-test-wordpress-version
new file mode 100644
index 00000000000..4cccf692ea0
--- /dev/null
+++ b/plugins/woocommerce/changelog/50016-skip-cys-test-wordpress-version
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+CYS - Run appropriate tests depending on the WordPress version.
\ No newline at end of file
diff --git a/packages/js/experimental/changelog/45148-poc-request-reviews b/plugins/woocommerce/changelog/50080-fix-revisit-opt-in-flow-patterns
similarity index 50%
rename from packages/js/experimental/changelog/45148-poc-request-reviews
rename to plugins/woocommerce/changelog/50080-fix-revisit-opt-in-flow-patterns
index cddd7270de5..33aefeeb400 100644
--- a/packages/js/experimental/changelog/45148-poc-request-reviews
+++ b/plugins/woocommerce/changelog/50080-fix-revisit-opt-in-flow-patterns
@@ -1,4 +1,4 @@
Significance: minor
Type: update
-Bump node version.
\ No newline at end of file
+CYS: Improve opt-in flow patterns.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/50083-49192-update-show-on-front-setting b/plugins/woocommerce/changelog/50083-49192-update-show-on-front-setting
new file mode 100644
index 00000000000..8da769f8bde
--- /dev/null
+++ b/plugins/woocommerce/changelog/50083-49192-update-show-on-front-setting
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+CYS - Update the "show_on_front" setting to "posts" to avoid overriding the "page" template.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/48803-48783-cys-revisit-sidebar-layout b/plugins/woocommerce/changelog/50086-fix-opt-in-fonts
similarity index 52%
rename from plugins/woocommerce/changelog/48803-48783-cys-revisit-sidebar-layout
rename to plugins/woocommerce/changelog/50086-fix-opt-in-fonts
index 3728df6df03..3fb992150ab 100644
--- a/plugins/woocommerce/changelog/48803-48783-cys-revisit-sidebar-layout
+++ b/plugins/woocommerce/changelog/50086-fix-opt-in-fonts
@@ -1,4 +1,4 @@
Significance: minor
Type: update
-CYS: Revisit sidebar layout.
\ No newline at end of file
+CYS: Improve opt-in flow fonts.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/50127-fix-47557-display-address-card-for-virtual-products b/plugins/woocommerce/changelog/50127-fix-47557-display-address-card-for-virtual-products
new file mode 100644
index 00000000000..eabec756bb8
--- /dev/null
+++ b/plugins/woocommerce/changelog/50127-fix-47557-display-address-card-for-virtual-products
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Display address card for virtual products if shopper's address is known
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/50136-update-46209-text-adjustments-on-shipping-zones-settings-page b/plugins/woocommerce/changelog/50136-update-46209-text-adjustments-on-shipping-zones-settings-page
new file mode 100644
index 00000000000..01b271bff23
--- /dev/null
+++ b/plugins/woocommerce/changelog/50136-update-46209-text-adjustments-on-shipping-zones-settings-page
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Text adjustments on shipping zones settings page
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/50145-patch-4 b/plugins/woocommerce/changelog/50145-patch-4
new file mode 100644
index 00000000000..ecd473d13c0
--- /dev/null
+++ b/plugins/woocommerce/changelog/50145-patch-4
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+Comment: Fix typo (class-wc-rest-wccom-site-ssr-controller.php)
+
diff --git a/plugins/woocommerce/changelog/add-41443-lint-strict-types-directive-for-new-files b/plugins/woocommerce/changelog/add-41443-lint-strict-types-directive-for-new-files
deleted file mode 100644
index fb132389194..00000000000
--- a/plugins/woocommerce/changelog/add-41443-lint-strict-types-directive-for-new-files
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Lint new PHP files for strict types directive
diff --git a/plugins/woocommerce/changelog/add-42529-account-created-notice b/plugins/woocommerce/changelog/add-42529-account-created-notice
deleted file mode 100644
index 88c153b9a1d..00000000000
--- a/plugins/woocommerce/changelog/add-42529-account-created-notice
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: add
-
-Added notice to the order confirmation page for new accounts instructing them to change the default password.
diff --git a/plugins/woocommerce/changelog/add-44144-front-end-context b/plugins/woocommerce/changelog/add-44144-front-end-context
deleted file mode 100644
index 3b3ba2a9041..00000000000
--- a/plugins/woocommerce/changelog/add-44144-front-end-context
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: enhancement
-
-Provide the location context within the Product Collection block context
diff --git a/plugins/woocommerce/changelog/add-46918 b/plugins/woocommerce/changelog/add-46918
deleted file mode 100644
index 1db50792645..00000000000
--- a/plugins/woocommerce/changelog/add-46918
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: add
-
-Add a rest api to manage the product custom fields
diff --git a/plugins/woocommerce/changelog/add-46919 b/plugins/woocommerce/changelog/add-46919
deleted file mode 100644
index b89bc1d58ab..00000000000
--- a/plugins/woocommerce/changelog/add-46919
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: tweak
-
-Add $wpdb->esc_like to the search criteria when searching for a product custom field name
diff --git a/plugins/woocommerce/changelog/add-49908_track_unit_tests b/plugins/woocommerce/changelog/add-49908_track_unit_tests
new file mode 100644
index 00000000000..1ce4a226e7d
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-49908_track_unit_tests
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Add unit tests for the product_add_publish track.
diff --git a/plugins/woocommerce/changelog/add-49927-main-payments-screen b/plugins/woocommerce/changelog/add-49927-main-payments-screen
new file mode 100644
index 00000000000..7670ea53eda
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-49927-main-payments-screen
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add reactified main payments screen
diff --git a/plugins/woocommerce/changelog/add-add-pattern-button-to-no-blocks-placeholder b/plugins/woocommerce/changelog/add-add-pattern-button-to-no-blocks-placeholder
new file mode 100644
index 00000000000..5ceb8715168
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-add-pattern-button-to-no-blocks-placeholder
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add Pattern button to no blocks view on the CYS assembler
diff --git a/plugins/woocommerce/changelog/add-collection-instances-telemetry b/plugins/woocommerce/changelog/add-collection-instances-telemetry
deleted file mode 100644
index 913072c8d4b..00000000000
--- a/plugins/woocommerce/changelog/add-collection-instances-telemetry
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: dev
-
-Product Collection: add tracking for block usage
diff --git a/plugins/woocommerce/changelog/add-front-matter-for-checkout-docs b/plugins/woocommerce/changelog/add-front-matter-for-checkout-docs
deleted file mode 100644
index 8c7db58834a..00000000000
--- a/plugins/woocommerce/changelog/add-front-matter-for-checkout-docs
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-preparing checkout blocks docs for dev docs site
diff --git a/plugins/woocommerce/changelog/add-initial-install-option-value b/plugins/woocommerce/changelog/add-initial-install-option-value
deleted file mode 100644
index f33ab6d9cc3..00000000000
--- a/plugins/woocommerce/changelog/add-initial-install-option-value
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: tweak
-
-Add the initially installed WooCommerce version to the wp_options table
diff --git a/plugins/woocommerce/changelog/add-php-mc-stats b/plugins/woocommerce/changelog/add-php-mc-stats
new file mode 100644
index 00000000000..cf7db2f0bf3
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-php-mc-stats
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Use MC Stats for PHP fatal error counting
diff --git a/plugins/woocommerce/changelog/add-reactify-classic-payments-settings-feature-flag b/plugins/woocommerce/changelog/add-reactify-classic-payments-settings-feature-flag
new file mode 100644
index 00000000000..de54eabf17a
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-reactify-classic-payments-settings-feature-flag
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add reactify-classic-payments-settings feature flag
diff --git a/plugins/woocommerce/changelog/add-reactify-offline-wcpay-settings-pages b/plugins/woocommerce/changelog/add-reactify-offline-wcpay-settings-pages
new file mode 100644
index 00000000000..01d4df29207
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-reactify-offline-wcpay-settings-pages
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Render a React placeholder for offline and WooCommerce Payments settings sections
diff --git a/plugins/woocommerce/changelog/add-unique-id-field b/plugins/woocommerce/changelog/add-unique-id-field
deleted file mode 100644
index 1ba5e6f7e00..00000000000
--- a/plugins/woocommerce/changelog/add-unique-id-field
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: add
-
-Adds global_unique_id field to product and product variations
diff --git a/plugins/woocommerce/changelog/ci-add-new-daily-checks b/plugins/woocommerce/changelog/ci-add-new-daily-checks
deleted file mode 100644
index 87d171ace93..00000000000
--- a/plugins/woocommerce/changelog/ci-add-new-daily-checks
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Add daily checks for core e2e with PHP 8.1 and WP latest-1
diff --git a/plugins/woocommerce/changelog/ci-disable-perf-checks-for-prs b/plugins/woocommerce/changelog/ci-disable-perf-checks-for-prs
deleted file mode 100644
index bf509e22b9e..00000000000
--- a/plugins/woocommerce/changelog/ci-disable-perf-checks-for-prs
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Remove performance tests from PR checks (leave on push to trunk)
diff --git a/plugins/woocommerce/changelog/dev-49582_use_custom_link_for_sku_error b/plugins/woocommerce/changelog/dev-49582_use_custom_link_for_sku_error
new file mode 100644
index 00000000000..2dc0e3065f9
--- /dev/null
+++ b/plugins/woocommerce/changelog/dev-49582_use_custom_link_for_sku_error
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Fix E2E tests SKU field id #49729
diff --git a/plugins/woocommerce/changelog/dev-ci-caching-rework-playwright-caching b/plugins/woocommerce/changelog/dev-ci-caching-rework-playwright-caching
deleted file mode 100644
index 5ee7daf7ffd..00000000000
--- a/plugins/woocommerce/changelog/dev-ci-caching-rework-playwright-caching
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-CI: tuning deps caching for playwright.
diff --git a/plugins/woocommerce/changelog/dev-ci-caching-tweaks b/plugins/woocommerce/changelog/dev-ci-caching-tweaks
deleted file mode 100644
index 8f7b9acb5b9..00000000000
--- a/plugins/woocommerce/changelog/dev-ci-caching-tweaks
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Tweaks related to caching Composer dependecies and Playwright downloads in CI.
diff --git a/plugins/woocommerce/changelog/dev-ci-phpunit-sharding b/plugins/woocommerce/changelog/dev-ci-phpunit-sharding
new file mode 100644
index 00000000000..9be5fd6a8cd
--- /dev/null
+++ b/plugins/woocommerce/changelog/dev-ci-phpunit-sharding
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+CI: introduce PHPUnit tests sharding.
diff --git a/plugins/woocommerce/changelog/dev-ci-tweaks-phpunit-runs b/plugins/woocommerce/changelog/dev-ci-tweaks-phpunit-runs
deleted file mode 100644
index b1fa83d9e21..00000000000
--- a/plugins/woocommerce/changelog/dev-ci-tweaks-phpunit-runs
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-CI: reduce running time for PHPUnit related jobs.
diff --git a/plugins/woocommerce/changelog/dev-fix-double-posting-review-comment b/plugins/woocommerce/changelog/dev-fix-double-posting-review-comment
new file mode 100644
index 00000000000..5f8b4602154
--- /dev/null
+++ b/plugins/woocommerce/changelog/dev-fix-double-posting-review-comment
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Updated the workflow prompting for testing instructions to only run once (preventing double comments)
diff --git a/plugins/woocommerce/changelog/dev-fix-validating-strict-types-directive b/plugins/woocommerce/changelog/dev-fix-validating-strict-types-directive
deleted file mode 100644
index 16def0b041d..00000000000
--- a/plugins/woocommerce/changelog/dev-fix-validating-strict-types-directive
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-CI: buffix for linting missing strict types directive.
diff --git a/plugins/woocommerce/changelog/dev-speedup-deps-installation b/plugins/woocommerce/changelog/dev-speedup-deps-installation
deleted file mode 100644
index 96792c61bad..00000000000
--- a/plugins/woocommerce/changelog/dev-speedup-deps-installation
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Build: speedup dependencies installation by disabling composer optimize autoloading by default.
diff --git a/plugins/woocommerce/changelog/dev-tune-up-zip-generation b/plugins/woocommerce/changelog/dev-tune-up-zip-generation
deleted file mode 100644
index 76c199f300f..00000000000
--- a/plugins/woocommerce/changelog/dev-tune-up-zip-generation
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Minor tooling tweaks (zip compression level, composer invocation)
diff --git a/plugins/woocommerce/changelog/dev-update-playwright-1_45 b/plugins/woocommerce/changelog/dev-update-playwright-1_45
deleted file mode 100644
index 9a4619c822b..00000000000
--- a/plugins/woocommerce/changelog/dev-update-playwright-1_45
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Update Playwright from 1.44 to 1.45
diff --git a/plugins/woocommerce/changelog/dev-webpack-loaders-scannig-paths-tweaks b/plugins/woocommerce/changelog/dev-webpack-loaders-scannig-paths-tweaks
new file mode 100644
index 00000000000..30f765e3fca
--- /dev/null
+++ b/plugins/woocommerce/changelog/dev-webpack-loaders-scannig-paths-tweaks
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Monorepo: tweak Webpack loaders paths filtering for better build perfromance.
diff --git a/plugins/woocommerce/changelog/drop-select2 b/plugins/woocommerce/changelog/drop-select2
deleted file mode 100644
index e3c6835860d..00000000000
--- a/plugins/woocommerce/changelog/drop-select2
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Dropping select2 and point it to SelectWoo
diff --git a/plugins/woocommerce/changelog/e2e-add-allure-to-blocks-e2e b/plugins/woocommerce/changelog/e2e-add-allure-to-blocks-e2e
deleted file mode 100644
index 75456116d0d..00000000000
--- a/plugins/woocommerce/changelog/e2e-add-allure-to-blocks-e2e
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Add Allure to Blocks e2e tests
diff --git a/plugins/woocommerce/changelog/e2e-add-environment-reporter b/plugins/woocommerce/changelog/e2e-add-environment-reporter
new file mode 100644
index 00000000000..4c4036f10ea
--- /dev/null
+++ b/plugins/woocommerce/changelog/e2e-add-environment-reporter
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+E2E tests: add environment reporter
diff --git a/plugins/woocommerce/changelog/e2e-tweak-pw-config-retries-and-artifacts b/plugins/woocommerce/changelog/e2e-cleanup-environment-console-log
similarity index 100%
rename from plugins/woocommerce/changelog/e2e-tweak-pw-config-retries-and-artifacts
rename to plugins/woocommerce/changelog/e2e-cleanup-environment-console-log
diff --git a/plugins/woocommerce/changelog/e2e-fix-cart-checkout-tax-test b/plugins/woocommerce/changelog/e2e-fix-cart-checkout-tax-test
new file mode 100644
index 00000000000..ec525a7f777
--- /dev/null
+++ b/plugins/woocommerce/changelog/e2e-fix-cart-checkout-tax-test
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+E2E tests: Removed unnecessary pause in the test
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/e2e-fix-check-attributes-done b/plugins/woocommerce/changelog/e2e-fix-check-attributes-done
deleted file mode 100644
index 1f6c96b01cb..00000000000
--- a/plugins/woocommerce/changelog/e2e-fix-check-attributes-done
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Final sanity check to make sure attributes are done saving
diff --git a/plugins/woocommerce/changelog/e2e-fix-flaky-account-email-receiving b/plugins/woocommerce/changelog/e2e-fix-flaky-account-email-receiving
deleted file mode 100644
index 52b73daf2c1..00000000000
--- a/plugins/woocommerce/changelog/e2e-fix-flaky-account-email-receiving
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-E2E tests: Fix flaky account email receiving test
diff --git a/plugins/woocommerce/changelog/e2e-fix-flaky-connect-to-woo b/plugins/woocommerce/changelog/e2e-fix-flaky-connect-to-woo
deleted file mode 100644
index d723d8a590b..00000000000
--- a/plugins/woocommerce/changelog/e2e-fix-flaky-connect-to-woo
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-E2E tests: Fix flaky connect to Woo.com test
diff --git a/plugins/woocommerce/changelog/e2e-fix-flaky-create-variable-fill-regular-price-inventory-tab b/plugins/woocommerce/changelog/e2e-fix-flaky-create-variable-fill-regular-price-inventory-tab
deleted file mode 100644
index 76577ef674e..00000000000
--- a/plugins/woocommerce/changelog/e2e-fix-flaky-create-variable-fill-regular-price-inventory-tab
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-E2E tests: Fix flaky filling regular price in the inventory tab
diff --git a/plugins/woocommerce/changelog/e2e-fix-flaky-cys-footer-and-filling-sku b/plugins/woocommerce/changelog/e2e-fix-flaky-cys-footer-and-filling-sku
deleted file mode 100644
index d5e1559cebf..00000000000
--- a/plugins/woocommerce/changelog/e2e-fix-flaky-cys-footer-and-filling-sku
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-E2E tests: Fix flaky filling SKU field and CYS footer
diff --git a/plugins/woocommerce/changelog/e2e-fix-flaky-gutenberg-services-tests b/plugins/woocommerce/changelog/e2e-fix-flaky-gutenberg-services-tests
deleted file mode 100644
index 50932fd8833..00000000000
--- a/plugins/woocommerce/changelog/e2e-fix-flaky-gutenberg-services-tests
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-E2E tests: Fix flaky Gutenberg, WC Services tests
diff --git a/plugins/woocommerce/changelog/e2e-fix-flaky-test-filling-sku-in-product-editor b/plugins/woocommerce/changelog/e2e-fix-flaky-test-filling-sku-in-product-editor
deleted file mode 100644
index d53f176cc49..00000000000
--- a/plugins/woocommerce/changelog/e2e-fix-flaky-test-filling-sku-in-product-editor
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-E2E tests: Fix flaky merchant filling sku field in the new editor
diff --git a/plugins/woocommerce/changelog/e2e-fix-gutenberg-flaky-tests b/plugins/woocommerce/changelog/e2e-fix-gutenberg-flaky-tests
deleted file mode 100644
index 3ae0f6eb50a..00000000000
--- a/plugins/woocommerce/changelog/e2e-fix-gutenberg-flaky-tests
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-E2E tests: Fix flaky Gutenberg tests
diff --git a/plugins/woocommerce/changelog/e2e-fix-improve-retry-on-coupons b/plugins/woocommerce/changelog/e2e-fix-improve-retry-on-coupons
deleted file mode 100644
index 2b6e063bbb4..00000000000
--- a/plugins/woocommerce/changelog/e2e-fix-improve-retry-on-coupons
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Update all values to be random so that retries don't fail on assertions
diff --git a/plugins/woocommerce/changelog/e2e-fix-merchant-settings-general b/plugins/woocommerce/changelog/e2e-fix-merchant-settings-general
deleted file mode 100644
index 072ab5c5e7f..00000000000
--- a/plugins/woocommerce/changelog/e2e-fix-merchant-settings-general
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-E2E tests: Fix merchant settings general test
diff --git a/plugins/woocommerce/changelog/e2e-fix-settings-tax-test b/plugins/woocommerce/changelog/e2e-fix-settings-tax-test
deleted file mode 100644
index ec58d08b567..00000000000
--- a/plugins/woocommerce/changelog/e2e-fix-settings-tax-test
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-E2E tests: fix failing settings-tax e2e test
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/e2e-move-buildkite-test-collector-to-dev-dependencies b/plugins/woocommerce/changelog/e2e-move-buildkite-test-collector-to-dev-dependencies
deleted file mode 100644
index 9afa39ee178..00000000000
--- a/plugins/woocommerce/changelog/e2e-move-buildkite-test-collector-to-dev-dependencies
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Move buildkite-test-collector to devDependencies
diff --git a/plugins/woocommerce/changelog/e2e-support-external-sites b/plugins/woocommerce/changelog/e2e-support-external-sites
deleted file mode 100644
index f83fcbaad58..00000000000
--- a/plugins/woocommerce/changelog/e2e-support-external-sites
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Add support for e2e testing against external sites in CI
diff --git a/plugins/woocommerce/changelog/e2e-update-readme-pnpm-commands b/plugins/woocommerce/changelog/e2e-update-readme-pnpm-commands
new file mode 100644
index 00000000000..4f73f5cc6c2
--- /dev/null
+++ b/plugins/woocommerce/changelog/e2e-update-readme-pnpm-commands
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+
diff --git a/plugins/woocommerce/changelog/enhance-46536-product-collection-insertion b/plugins/woocommerce/changelog/enhance-46536-product-collection-insertion
deleted file mode 100644
index b20fd882e81..00000000000
--- a/plugins/woocommerce/changelog/enhance-46536-product-collection-insertion
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-Redesigned the Product Collection block's insertion journey.
diff --git a/plugins/woocommerce/changelog/feature-37597-37598-inform-screen-reader-users-when-mini-cart-updates b/plugins/woocommerce/changelog/feature-37597-37598-inform-screen-reader-users-when-mini-cart-updates
new file mode 100644
index 00000000000..fa6d46c15ab
--- /dev/null
+++ b/plugins/woocommerce/changelog/feature-37597-37598-inform-screen-reader-users-when-mini-cart-updates
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Inform screen reader users when mini cart updates
diff --git a/plugins/woocommerce/changelog/fix-35290 b/plugins/woocommerce/changelog/fix-35290
deleted file mode 100644
index d86a0bf4305..00000000000
--- a/plugins/woocommerce/changelog/fix-35290
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Return HTTP 404 when accessing non-existent webhooks via REST API.
diff --git a/plugins/woocommerce/changelog/fix-35291 b/plugins/woocommerce/changelog/fix-35291
deleted file mode 100644
index f2cd9c5c8e4..00000000000
--- a/plugins/woocommerce/changelog/fix-35291
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Return HTTP 404 for REST API requests involving non-existing tax class.
diff --git a/plugins/woocommerce/changelog/fix-36748-analytics-dashboard-currency-formatting b/plugins/woocommerce/changelog/fix-36748-analytics-dashboard-currency-formatting
deleted file mode 100644
index 70dd09fea89..00000000000
--- a/plugins/woocommerce/changelog/fix-36748-analytics-dashboard-currency-formatting
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Make the Leaderboards on the Analytics > Dashboard page use consistent currency and number formatting across the page, and perceive the currency setting comes from the relevant filter.
diff --git a/plugins/woocommerce/changelog/fix-38384-esc-html b/plugins/woocommerce/changelog/fix-38384-esc-html
deleted file mode 100644
index 85362ae99db..00000000000
--- a/plugins/woocommerce/changelog/fix-38384-esc-html
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Correctly escape the HTML when linking customer orders.
diff --git a/plugins/woocommerce/changelog/fix-39318-remove-enctype b/plugins/woocommerce/changelog/fix-39318-remove-enctype
deleted file mode 100644
index b977b6d5cb9..00000000000
--- a/plugins/woocommerce/changelog/fix-39318-remove-enctype
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-
-Remove enctype from verify email form
diff --git a/plugins/woocommerce/changelog/fix-40688-control-tk_ai-cookie b/plugins/woocommerce/changelog/fix-40688-control-tk_ai-cookie
deleted file mode 100644
index b77a2c64bb2..00000000000
--- a/plugins/woocommerce/changelog/fix-40688-control-tk_ai-cookie
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Do not set the `tk_ai` tracking cookie if tracking is disabled.
diff --git a/plugins/woocommerce/changelog/fix-41373 b/plugins/woocommerce/changelog/fix-41373
deleted file mode 100644
index 19bf8a6b88f..00000000000
--- a/plugins/woocommerce/changelog/fix-41373
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Ensures the product ID is valid when interacting with product variations via the REST API.
diff --git a/plugins/woocommerce/changelog/fix-43628-view-button-orders-page b/plugins/woocommerce/changelog/fix-43628-view-button-orders-page
new file mode 100644
index 00000000000..5027ce08bf6
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-43628-view-button-orders-page
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Add aria-label on View order button to aid in accessibility for screen readers
diff --git a/plugins/woocommerce/changelog/fix-43658-add-required-indication-to-login-form b/plugins/woocommerce/changelog/fix-43658-add-required-indication-to-login-form
deleted file mode 100644
index 1d106bee55b..00000000000
--- a/plugins/woocommerce/changelog/fix-43658-add-required-indication-to-login-form
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: enhancement
-
-Add required indication to login forms
diff --git a/plugins/woocommerce/changelog/fix-44359 b/plugins/woocommerce/changelog/fix-44359
deleted file mode 100644
index 22d152e4ca3..00000000000
--- a/plugins/woocommerce/changelog/fix-44359
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: performance
-
-Load REST API namespaces only when needed.
diff --git a/plugins/woocommerce/changelog/fix-47601-rest-api-refunds b/plugins/woocommerce/changelog/fix-47601-rest-api-refunds
deleted file mode 100644
index d57bf12c299..00000000000
--- a/plugins/woocommerce/changelog/fix-47601-rest-api-refunds
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Provide more informative errors if a refund cannot be requested via the REST API, due to plugin conflicts.
diff --git a/plugins/woocommerce/changelog/fix-47655-stack-coupon-fields b/plugins/woocommerce/changelog/fix-47655-stack-coupon-fields
deleted file mode 100644
index 14a819d3287..00000000000
--- a/plugins/woocommerce/changelog/fix-47655-stack-coupon-fields
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Made coupon fields during block checkout stack on smaller screen sizes
diff --git a/plugins/woocommerce/changelog/fix-47975-overlay-navigation-block b/plugins/woocommerce/changelog/fix-47975-overlay-navigation-block
deleted file mode 100644
index 9e2bdd31337..00000000000
--- a/plugins/woocommerce/changelog/fix-47975-overlay-navigation-block
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: add
-Comment: Adding an experimental block called Navigation, which is an inner block of Product Filters Overlay to close the overlay w when openned.
-
-
diff --git a/plugins/woocommerce/changelog/fix-48022-product-filters-block-spacing b/plugins/woocommerce/changelog/fix-48022-product-filters-block-spacing
deleted file mode 100644
index 2446e4b86d2..00000000000
--- a/plugins/woocommerce/changelog/fix-48022-product-filters-block-spacing
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: add
-Comment: Add layout and blockGap supports to the Product Filters block.
-
-
diff --git a/plugins/woocommerce/changelog/fix-48126-legacy-status-handling b/plugins/woocommerce/changelog/fix-48126-legacy-status-handling
deleted file mode 100644
index 109a5109098..00000000000
--- a/plugins/woocommerce/changelog/fix-48126-legacy-status-handling
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Update allowed statuses in legacy payment handler for checkout block.
diff --git a/plugins/woocommerce/changelog/fix-48285-improve-classic-template-block-registration b/plugins/woocommerce/changelog/fix-48285-improve-classic-template-block-registration
deleted file mode 100644
index 1174bbd669a..00000000000
--- a/plugins/woocommerce/changelog/fix-48285-improve-classic-template-block-registration
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Fix Classic Template block registration on WP 6.6
diff --git a/plugins/woocommerce/changelog/fix-48431-blurry-thumbnail-images b/plugins/woocommerce/changelog/fix-48431-blurry-thumbnail-images
deleted file mode 100644
index 0ebb744a4b1..00000000000
--- a/plugins/woocommerce/changelog/fix-48431-blurry-thumbnail-images
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-Make Single Product gallery thumbnail images sharper by defining a srcset
diff --git a/plugins/woocommerce/changelog/fix-48489-ensure-correct-tempalte-file-is-used-in-editor b/plugins/woocommerce/changelog/fix-48489-ensure-correct-tempalte-file-is-used-in-editor
deleted file mode 100644
index 932799d3e86..00000000000
--- a/plugins/woocommerce/changelog/fix-48489-ensure-correct-tempalte-file-is-used-in-editor
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Make sure the correct block template file is used in the Site Editor for templates with fallback
diff --git a/plugins/woocommerce/changelog/fix-48505-notice-template-override b/plugins/woocommerce/changelog/fix-48505-notice-template-override
deleted file mode 100644
index 4d09a231ab1..00000000000
--- a/plugins/woocommerce/changelog/fix-48505-notice-template-override
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Narrowed scope of block theme notice templates so other template overrides are unaffected
diff --git a/plugins/woocommerce/changelog/fix-48536-product-search-results-fallback b/plugins/woocommerce/changelog/fix-48536-product-search-results-fallback
deleted file mode 100644
index 0f7c6eb1df7..00000000000
--- a/plugins/woocommerce/changelog/fix-48536-product-search-results-fallback
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-Avoid Product Search Results block template to fall back to the Product Catalog template from the theme
diff --git a/plugins/woocommerce/changelog/fix-49423-allow-rendering-the-quantity-in-cart-when-the-quantity-is-not-editable b/plugins/woocommerce/changelog/fix-49423-allow-rendering-the-quantity-in-cart-when-the-quantity-is-not-editable
new file mode 100644
index 00000000000..b3680215875
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-49423-allow-rendering-the-quantity-in-cart-when-the-quantity-is-not-editable
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+allows the quantity selector on block cart page to render as readonly when editable is false
diff --git a/plugins/woocommerce/changelog/fix-49578-new-attribute-filter-inspector-settings b/plugins/woocommerce/changelog/fix-49578-new-attribute-filter-inspector-settings
new file mode 100644
index 00000000000..d7ddbb723c4
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-49578-new-attribute-filter-inspector-settings
@@ -0,0 +1,5 @@
+Significance: patch
+Type: update
+Comment: [Experimental] Attribute Filter: Update Inspector Control Settings
+
+
diff --git a/plugins/woocommerce/changelog/fix-49980-overlay-navigation-title-icon b/plugins/woocommerce/changelog/fix-49980-overlay-navigation-title-icon
new file mode 100644
index 00000000000..22e9c27ed8d
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-49980-overlay-navigation-title-icon
@@ -0,0 +1,3 @@
+Significance: patch
+Type: update
+Comment: New title and icon for Overlay Navigation block
diff --git a/plugins/woocommerce/changelog/fix-add-back-revert-template-tests b/plugins/woocommerce/changelog/fix-add-back-revert-template-tests
deleted file mode 100644
index b9682c30fa2..00000000000
--- a/plugins/woocommerce/changelog/fix-add-back-revert-template-tests
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: dev
-Comment: Add back revert template e2e tests
-
-
diff --git a/plugins/woocommerce/changelog/fix-blockified-templates-readme-link-to-docs b/plugins/woocommerce/changelog/fix-blockified-templates-readme-link-to-docs
deleted file mode 100644
index b0618088661..00000000000
--- a/plugins/woocommerce/changelog/fix-blockified-templates-readme-link-to-docs
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Add a link to the Theming docs from the blockified templates README.md file
diff --git a/plugins/woocommerce/changelog/fix-codesniffer-tweaks-conflicting-rules b/plugins/woocommerce/changelog/fix-codesniffer-tweaks-conflicting-rules
deleted file mode 100644
index 07596b7d081..00000000000
--- a/plugins/woocommerce/changelog/fix-codesniffer-tweaks-conflicting-rules
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: dev
-
-Updated CodeSniffer configuration to address conflicting rules.
diff --git a/plugins/woocommerce/changelog/fix-coming-soon-heading-color b/plugins/woocommerce/changelog/fix-coming-soon-heading-color
deleted file mode 100644
index e92b3feb7f8..00000000000
--- a/plugins/woocommerce/changelog/fix-coming-soon-heading-color
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fix site coming soon page heading color
diff --git a/plugins/woocommerce/changelog/fix-downloadable-product-metabox-toggle b/plugins/woocommerce/changelog/fix-downloadable-product-metabox-toggle
deleted file mode 100644
index d9dc14eb308..00000000000
--- a/plugins/woocommerce/changelog/fix-downloadable-product-metabox-toggle
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Prevent download permissions metabox from being toggled when toggling individual permission details.
diff --git a/plugins/woocommerce/changelog/fix-exclude-coming-soon-patterns-from-block-inserter b/plugins/woocommerce/changelog/fix-exclude-coming-soon-patterns-from-block-inserter
deleted file mode 100644
index c12cd9b632f..00000000000
--- a/plugins/woocommerce/changelog/fix-exclude-coming-soon-patterns-from-block-inserter
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-
-Exclude coming soon patterns from block inserter
diff --git a/plugins/woocommerce/changelog/fix-l10n-too-early b/plugins/woocommerce/changelog/fix-l10n-too-early
deleted file mode 100644
index 54a3a18947b..00000000000
--- a/plugins/woocommerce/changelog/fix-l10n-too-early
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-Removes several side effects in the code bases that caused translations to be loaded too early.
diff --git a/plugins/woocommerce/changelog/fix-logout-endpoint-handling-25288 b/plugins/woocommerce/changelog/fix-logout-endpoint-handling-25288
new file mode 100644
index 00000000000..bad687d736c
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-logout-endpoint-handling-25288
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Fixed log-out link behavior so that redirects work, and so that security nonces are automatically added to link in navigation menus.
diff --git a/plugins/woocommerce/changelog/fix-lys-refetch-site-cache-status b/plugins/woocommerce/changelog/fix-lys-refetch-site-cache-status
deleted file mode 100644
index 8404d5521ed..00000000000
--- a/plugins/woocommerce/changelog/fix-lys-refetch-site-cache-status
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fetch site cache status correctly if directly navigating to LYS Success page, and some refactoring
diff --git a/plugins/woocommerce/changelog/fix-option-notice-analytics-orders b/plugins/woocommerce/changelog/fix-option-notice-analytics-orders
deleted file mode 100644
index 2854bac4da4..00000000000
--- a/plugins/woocommerce/changelog/fix-option-notice-analytics-orders
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fix get_options deprecated notice when viewing Analytics > Orders
diff --git a/plugins/woocommerce/changelog/fix-page-titles-49798 b/plugins/woocommerce/changelog/fix-page-titles-49798
new file mode 100644
index 00000000000..957f2bd8467
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-page-titles-49798
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix page titles of the cart and checkout page when using blocks and FSE themes.
diff --git a/plugins/woocommerce/changelog/fix-payment-settings-remove-save-changes-button b/plugins/woocommerce/changelog/fix-payment-settings-remove-save-changes-button
new file mode 100644
index 00000000000..91dbfd1b8b1
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-payment-settings-remove-save-changes-button
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Hide save changes button in main payments screen
diff --git a/plugins/woocommerce/changelog/fix-payments-apms-tasks-logic-and-links b/plugins/woocommerce/changelog/fix-payments-apms-tasks-logic-and-links
deleted file mode 100644
index 818ab763234..00000000000
--- a/plugins/woocommerce/changelog/fix-payments-apms-tasks-logic-and-links
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: fix
-Comment: Fixes for keeping the WooPayments and general payments tasks mutually exclusive and predictable.
-
-
diff --git a/plugins/woocommerce/changelog/fix-pc-max-price-filter-value-inclusion b/plugins/woocommerce/changelog/fix-pc-max-price-filter-value-inclusion
new file mode 100644
index 00000000000..2efb0084d96
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-pc-max-price-filter-value-inclusion
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Product Collection: Fix max price query to include prices less or equal to the given max value.
diff --git a/plugins/woocommerce/changelog/fix-placeholder-display-25152 b/plugins/woocommerce/changelog/fix-placeholder-display-25152
new file mode 100644
index 00000000000..40e5d7c046f
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-placeholder-display-25152
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fixed placeholders in the classic cart shipping calculator to update with country selection.
diff --git a/plugins/woocommerce/changelog/fix-rendering-unforced-blocks b/plugins/woocommerce/changelog/fix-rendering-unforced-blocks
deleted file mode 100644
index ab66767b296..00000000000
--- a/plugins/woocommerce/changelog/fix-rendering-unforced-blocks
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: enhancement
-
-Allow blocks with parents in the "woocommerce" namespace to be added to the Checkout block without requiring them to be added to the "__experimental_woocommerce_blocks_add_data_attributes_to_block" hook.
diff --git a/plugins/woocommerce/changelog/fix-script-loading-minicart b/plugins/woocommerce/changelog/fix-script-loading-minicart
deleted file mode 100644
index e6fd9f8ef1c..00000000000
--- a/plugins/woocommerce/changelog/fix-script-loading-minicart
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fixed a bug where the close button is not visible on the mini cart when viewed on a mobile device
diff --git a/plugins/woocommerce/changelog/fix-shipping-zones-add-zone-button-flinching b/plugins/woocommerce/changelog/fix-shipping-zones-add-zone-button-flinching
deleted file mode 100644
index b537a4f1bd1..00000000000
--- a/plugins/woocommerce/changelog/fix-shipping-zones-add-zone-button-flinching
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Fix add zone button flinching and vertical centering
diff --git a/plugins/woocommerce/changelog/fix-test-orders b/plugins/woocommerce/changelog/fix-test-orders
deleted file mode 100644
index 70703e36a94..00000000000
--- a/plugins/woocommerce/changelog/fix-test-orders
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: tweak
-
-Improve checks when offering to remove test orders
diff --git a/plugins/woocommerce/changelog/fix-view-store-button b/plugins/woocommerce/changelog/fix-view-store-button
deleted file mode 100644
index b58b8f701a8..00000000000
--- a/plugins/woocommerce/changelog/fix-view-store-button
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Display "View store" button text by default in the toolbar.
diff --git a/plugins/woocommerce/changelog/fix-warning-related-to-customer-meta-field-without-class b/plugins/woocommerce/changelog/fix-warning-related-to-customer-meta-field-without-class
new file mode 100644
index 00000000000..992f39ad9d1
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-warning-related-to-customer-meta-field-without-class
@@ -0,0 +1,6 @@
+Significance: patch
+Type: fix
+
+Reduce error noise in the user profile screen, by removing the requirement for custom fields to have a class attribute.
+
+
diff --git a/plugins/woocommerce/changelog/fix-wcadmin-react-18-create-root-shippingzones-payment-methods b/plugins/woocommerce/changelog/fix-wcadmin-react-18-create-root-shippingzones-payment-methods
new file mode 100644
index 00000000000..c42e8d67aed
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wcadmin-react-18-create-root-shippingzones-payment-methods
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Changed from using React.render to React.createRoot for payment methods promotion, shipping settings region zone as it has been deprecated since React 18
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/fix-wcadmin-react18-createroot-product-editor b/plugins/woocommerce/changelog/fix-wcadmin-react18-createroot-product-editor
deleted file mode 100644
index 04b9a752892..00000000000
--- a/plugins/woocommerce/changelog/fix-wcadmin-react18-createroot-product-editor
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Changed from using React.render to React.createRoot for product editor areas as it has been deprecated since React 18
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/pr-48169 b/plugins/woocommerce/changelog/pr-48169
deleted file mode 100644
index dfbd74c6f0e..00000000000
--- a/plugins/woocommerce/changelog/pr-48169
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: add
-
-Improvements in the handling of feature compatibility for plugins
diff --git a/plugins/woocommerce/changelog/pr-48884 b/plugins/woocommerce/changelog/pr-48884
deleted file mode 100644
index c463a3afd15..00000000000
--- a/plugins/woocommerce/changelog/pr-48884
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Improve the handling of the deprecated WC()->api property
diff --git a/plugins/woocommerce/changelog/prep-post-release-tasks-9.1.4 b/plugins/woocommerce/changelog/prep-post-release-tasks-9.1.4
new file mode 100644
index 00000000000..c0f6da403d4
--- /dev/null
+++ b/plugins/woocommerce/changelog/prep-post-release-tasks-9.1.4
@@ -0,0 +1,5 @@
+Significance: patch
+Type: dev
+Comment: Release task/automated change. No changelog needed.
+
+
diff --git a/plugins/woocommerce/changelog/prep-trunk-for-next-dev-cycle-9.3 b/plugins/woocommerce/changelog/prep-trunk-for-next-dev-cycle-9.3
new file mode 100644
index 00000000000..f9f79951091
--- /dev/null
+++ b/plugins/woocommerce/changelog/prep-trunk-for-next-dev-cycle-9.3
@@ -0,0 +1,5 @@
+Significance: patch
+Type: dev
+Comment: Automated version bump, there is nothing of value worth communicating via the changelog.
+
+
diff --git a/plugins/woocommerce/changelog/sku-check-before-product-creation b/plugins/woocommerce/changelog/sku-check-before-product-creation
deleted file mode 100644
index c8b7330f9f8..00000000000
--- a/plugins/woocommerce/changelog/sku-check-before-product-creation
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Ensuring product creation with unique sku for concurrent requests
diff --git a/plugins/woocommerce/changelog/tests-move-api-tests-into-e2e b/plugins/woocommerce/changelog/tests-move-api-tests-into-e2e
new file mode 100644
index 00000000000..c12f3a3d845
--- /dev/null
+++ b/plugins/woocommerce/changelog/tests-move-api-tests-into-e2e
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Tests: moved api core tests as a suite in e2e-pw
diff --git a/plugins/woocommerce/changelog/tweak-select-tree-2 b/plugins/woocommerce/changelog/tweak-select-tree-2
deleted file mode 100644
index 13f276452a1..00000000000
--- a/plugins/woocommerce/changelog/tweak-select-tree-2
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: fix
-Comment: Fix E2E test about Categories field
-
-
diff --git a/plugins/woocommerce/changelog/udpate-e2e-test-improve-add-variation-test b/plugins/woocommerce/changelog/udpate-e2e-test-improve-add-variation-test
deleted file mode 100644
index aaea8326a4c..00000000000
--- a/plugins/woocommerce/changelog/udpate-e2e-test-improve-add-variation-test
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Product Editor: restore and fix E2E test that creates product variations
diff --git a/plugins/woocommerce/changelog/update-43637-semi-sticky-proceed-to-checkout-button b/plugins/woocommerce/changelog/update-43637-semi-sticky-proceed-to-checkout-button
deleted file mode 100644
index 5330a955455..00000000000
--- a/plugins/woocommerce/changelog/update-43637-semi-sticky-proceed-to-checkout-button
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-Make proceed to order button non sticky when zoom level is bigger than 100%
diff --git a/plugins/woocommerce/changelog/update-48533-account-creation-settings b/plugins/woocommerce/changelog/update-48533-account-creation-settings
deleted file mode 100644
index 63db9e1ae21..00000000000
--- a/plugins/woocommerce/changelog/update-48533-account-creation-settings
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Updated account settings descriptions for added clarity
diff --git a/plugins/woocommerce/changelog/update-BlockTemplatesController-inline-methods b/plugins/woocommerce/changelog/update-BlockTemplatesController-inline-methods
deleted file mode 100644
index 9459d06260f..00000000000
--- a/plugins/woocommerce/changelog/update-BlockTemplatesController-inline-methods
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: BlockTemplatesController: move all hooks into methods instead of being inline
-
-
diff --git a/plugins/woocommerce/changelog/update-abrev-global-unique-id b/plugins/woocommerce/changelog/update-abrev-global-unique-id
new file mode 100644
index 00000000000..d35e7d8f1e6
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-abrev-global-unique-id
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Add abbreviations for fields GTIN, UPC, EAN, OR ISBN
diff --git a/plugins/woocommerce/changelog/update-e2e-check-add-button-when-creating-new-global-attribute-terms b/plugins/woocommerce/changelog/update-e2e-check-add-button-when-creating-new-global-attribute-terms
deleted file mode 100644
index e944b9b4662..00000000000
--- a/plugins/woocommerce/changelog/update-e2e-check-add-button-when-creating-new-global-attribute-terms
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-E2E: check the `Add` button when creating product variations in the new Product Editor
diff --git a/plugins/woocommerce/changelog/update-e2e-checking-new-tokens b/plugins/woocommerce/changelog/update-e2e-checking-new-tokens
deleted file mode 100644
index 82df4fcb9f2..00000000000
--- a/plugins/woocommerce/changelog/update-e2e-checking-new-tokens
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-E2E: remove UI check when creating attribute global terms
diff --git a/plugins/woocommerce/changelog/update-e2e-iterate-how-wait-for-global-attributes-loaded b/plugins/woocommerce/changelog/update-e2e-iterate-how-wait-for-global-attributes-loaded
deleted file mode 100644
index d5704c98083..00000000000
--- a/plugins/woocommerce/changelog/update-e2e-iterate-how-wait-for-global-attributes-loaded
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-E2E: in the new Product Editor app, update how to detect when global attributes are loaded.
diff --git a/plugins/woocommerce/changelog/update-product-editor-polish-e2e-tests b/plugins/woocommerce/changelog/update-product-editor-polish-e2e-tests
deleted file mode 100644
index d77dec7c2da..00000000000
--- a/plugins/woocommerce/changelog/update-product-editor-polish-e2e-tests
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Product Editor: improve E2E tests. Test the `+3 More` item label in the Organization tab
diff --git a/plugins/woocommerce/changelog/update-product-editor-restore-product-with-local-attrs-2e2-test b/plugins/woocommerce/changelog/update-product-editor-restore-product-with-local-attrs-2e2-test
deleted file mode 100644
index b7a1fb66169..00000000000
--- a/plugins/woocommerce/changelog/update-product-editor-restore-product-with-local-attrs-2e2-test
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Product Editor: restore Product (local) Attributes E2E test
diff --git a/plugins/woocommerce/changelog/update-product-editor-validate-term-tokens b/plugins/woocommerce/changelog/update-product-editor-validate-term-tokens
deleted file mode 100644
index 3ca383f0281..00000000000
--- a/plugins/woocommerce/changelog/update-product-editor-validate-term-tokens
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: update
-
-Product Editor: update create product variations E2E test
diff --git a/plugins/woocommerce/changelog/update-shipping-modal-copy b/plugins/woocommerce/changelog/update-shipping-modal-copy
deleted file mode 100644
index 9224631d266..00000000000
--- a/plugins/woocommerce/changelog/update-shipping-modal-copy
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: update
-
-Update shipping method setup modal copy if the block-based local pickup is enabled
diff --git a/plugins/woocommerce/changelog/update-ssr-update-api-item b/plugins/woocommerce/changelog/update-ssr-update-api-item
deleted file mode 100644
index 4fc19a74ec5..00000000000
--- a/plugins/woocommerce/changelog/update-ssr-update-api-item
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-
-Update the "API enabled" entry in the System Status Report to clarify that it pertains to the Legacy REST API.
diff --git a/plugins/woocommerce/changelog/update-wcpay-tracks-events-props b/plugins/woocommerce/changelog/update-wcpay-tracks-events-props
deleted file mode 100644
index 9e65194d5d1..00000000000
--- a/plugins/woocommerce/changelog/update-wcpay-tracks-events-props
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: Minor updates to WooPayments incentives page tracking props.
-
-
diff --git a/plugins/woocommerce/client/admin/config/core.json b/plugins/woocommerce/client/admin/config/core.json
index a26772d167d..3760e081f71 100644
--- a/plugins/woocommerce/client/admin/config/core.json
+++ b/plugins/woocommerce/client/admin/config/core.json
@@ -19,13 +19,13 @@
"navigation": true,
"onboarding": true,
"onboarding-tasks": true,
- "pattern-toolkit-full-composability": false,
+ "pattern-toolkit-full-composability": true,
"product-pre-publish-modal": false,
"product-custom-fields": true,
"remote-inbox-notifications": true,
"remote-free-extensions": true,
"payment-gateway-suggestions": true,
- "printful": false,
+ "printful": true,
"settings": false,
"shipping-label-banner": true,
"subscriptions": true,
@@ -36,6 +36,7 @@
"wc-pay-welcome-page": true,
"async-product-editor-category-field": false,
"launch-your-store": true,
- "product-editor-template-system": false
+ "product-editor-template-system": false,
+ "reactify-classic-payments-settings": false
}
}
diff --git a/plugins/woocommerce/client/admin/config/development.json b/plugins/woocommerce/client/admin/config/development.json
index 4343f131c6c..2b609c124eb 100644
--- a/plugins/woocommerce/client/admin/config/development.json
+++ b/plugins/woocommerce/client/admin/config/development.json
@@ -19,11 +19,11 @@
"navigation": true,
"onboarding": true,
"onboarding-tasks": true,
- "pattern-toolkit-full-composability": false,
+ "pattern-toolkit-full-composability": true,
"payment-gateway-suggestions": true,
"product-pre-publish-modal": false,
"product-custom-fields": true,
- "printful": false,
+ "printful": true,
"remote-inbox-notifications": true,
"remote-free-extensions": true,
"settings": false,
@@ -36,6 +36,7 @@
"wc-pay-welcome-page": true,
"async-product-editor-category-field": true,
"launch-your-store": true,
- "product-editor-template-system": false
+ "product-editor-template-system": false,
+ "reactify-classic-payments-settings": false
}
}
diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss
index 7b63dd7cca8..2c03041df6e 100644
--- a/plugins/woocommerce/client/legacy/css/admin.scss
+++ b/plugins/woocommerce/client/legacy/css/admin.scss
@@ -2698,12 +2698,13 @@ ul.wc_coupon_list_block {
color: #ccc;
display: inline-block;
cursor: pointer;
- padding: 0 0 0.5em;
- margin: 0 0 0 12px;
+ padding: 0;
+ margin: 0 0 6px 12px;
vertical-align: middle;
text-decoration: none;
line-height: 16px;
width: 16px;
+ height: 16px;
overflow: hidden;
&::before {
@@ -2744,11 +2745,14 @@ ul.wc_coupon_list_block {
}
tbody tr .wc-order-edit-line-item-actions {
- visibility: hidden;
+ opacity: 0;
}
- tbody tr:hover .wc-order-edit-line-item-actions {
- visibility: visible;
+ tbody tr:focus-within,
+ tbody tr:hover {
+ .wc-order-edit-line-item-actions {
+ opacity: 1;
+ }
}
.wc-order-totals .wc-order-edit-line-item-actions {
@@ -3069,6 +3073,10 @@ ul.wc_coupon_list_block {
.column-order_number {
width: 20ch;
+
+ .small-screen-only {
+ display: none;
+ }
}
.column-order_total {
@@ -3426,32 +3434,41 @@ ul.wc_coupon_list_block {
a.order-view {
display: inline-block;
- width: calc(100% - 48px - 10ch - 20ch);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+ vertical-align: top;
+ width: calc( 50% - 40px - 4px );
+ margin-right: 4px;
}
- }
- td.column-order_date {
- width: 10ch !important;
- top: 1em;
- right: calc(20ch + 48px);
- }
+ .small-screen-only {
+ display: inline-block;
+ vertical-align: top;
- td.column-order_status {
- top: -1em;
- right: 48px;
- }
+ &.order_date {
+ width: calc( 20% - 4px );
+ margin-right: 4px;
+ }
- td.column-order_status,
- td.column-order_date {
- padding: 0 !important;
- display: block !important;
- float: right;
+ &.order_status {
+ width: 30%;
+ text-align: right;
+ }
+ }
- &::before {
- display: none !important;
+ @media screen and (max-width: 400px) {
+ a.order-view {
+ display: block;
+ width: calc( 100% - 40px );
+ }
+
+ .small-screen-only {
+ &.order_date {
+ width: calc( 40% - 4px );
+ }
+
+ &.order_status {
+ width: calc( 60% - 40px);
+ }
+ }
}
}
}
@@ -6526,8 +6543,8 @@ img.help_tip {
.upload_image_button {
display: block;
- width: 64px;
- height: 64px;
+ width: 128px;
+ height: 128px;
float: left;
margin-right: 20px;
position: relative;
@@ -6548,8 +6565,8 @@ img.help_tip {
right: 0;
bottom: 0;
text-align: center;
- line-height: 64px;
- font-size: 64px;
+ line-height: 128px;
+ font-size: 128px;
font-weight: 400;
-webkit-font-smoothing: antialiased;
}
@@ -6656,6 +6673,12 @@ img.help_tip {
}
}
+.form-flex-box {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
/**
* Tooltips
*/
diff --git a/plugins/woocommerce/client/legacy/css/woocommerce.scss b/plugins/woocommerce/client/legacy/css/woocommerce.scss
index afa7956c29b..e82735b9dc2 100644
--- a/plugins/woocommerce/client/legacy/css/woocommerce.scss
+++ b/plugins/woocommerce/client/legacy/css/woocommerce.scss
@@ -80,14 +80,14 @@ p.demo_store,
text-align: center;
line-height: 1;
border-radius: 100%;
- color: red !important; // Required for default theme compatibility
+ color: var(--wc-red) !important; // Required for default theme compatibility
text-decoration: none;
font-weight: 700;
border: 0;
&:hover {
color: #fff !important; // Required for default theme compatibility
- background: red;
+ background: var(--wc-red);
}
}
@@ -1224,7 +1224,7 @@ p.demo_store,
}
.required {
- color: red;
+ color: var(--wc-red);
font-weight: 700;
border: 0 !important;
text-decoration: none;
@@ -1265,13 +1265,13 @@ p.demo_store,
&.woocommerce-invalid {
label {
- color: $red;
+ color: var(--wc-red);
}
.select2-container,
input.input-text,
select {
- border-color: $red;
+ border-color: var(--wc-red);
}
}
@@ -1521,7 +1521,7 @@ p.demo_store,
.woocommerce-widget-layered-nav-list__item--chosen a::before {
@include iconbefore("\e013");
- color: $red;
+ color: var(--wc-red);
}
}
@@ -1547,7 +1547,7 @@ p.demo_store,
&::before {
@include iconbefore("\e013");
- color: $red;
+ color: var(--wc-red);
vertical-align: inherit;
margin-right: 0.5em;
}
@@ -1657,7 +1657,7 @@ p.demo_store,
li.chosen a::before {
@include iconbefore("\e013");
- color: $red;
+ color: var(--wc-red);
}
}
@@ -1793,7 +1793,7 @@ p.demo_store,
}
.out-of-stock {
- color: red;
+ color: var(--wc-red);
}
}
@@ -2290,7 +2290,7 @@ p.demo_store,
.woocommerce-invalid {
#terms {
- outline: 2px solid red;
+ outline: 2px solid var(--wc-red);
outline-offset: 2px;
}
}
diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js
index 1504e1096c6..596a9edebdf 100644
--- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js
+++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js
@@ -636,6 +636,7 @@ jQuery( function ( $ ) {
} else {
window.alert( response.data.error );
}
+ wc_meta_boxes_order.init_tiptip();
wc_meta_boxes_order_items.unblock();
});
}
@@ -664,6 +665,7 @@ jQuery( function ( $ ) {
} else {
window.alert( response.data.error );
}
+ wc_meta_boxes_order.init_tiptip();
wc_meta_boxes_order_items.unblock();
});
diff --git a/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js
index 046b466e037..a99e4394750 100644
--- a/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js
+++ b/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js
@@ -9,16 +9,17 @@ jQuery( function( $ ) {
* AddToCartHandler class.
*/
var AddToCartHandler = function() {
- this.requests = [];
- this.addRequest = this.addRequest.bind( this );
- this.run = this.run.bind( this );
+ this.requests = [];
+ this.addRequest = this.addRequest.bind( this );
+ this.run = this.run.bind( this );
+ this.$liveRegion = this.createLiveRegion();
$( document.body )
.on( 'click', '.add_to_cart_button:not(.wc-interactive)', { addToCartHandler: this }, this.onAddToCart )
.on( 'click', '.remove_from_cart_button', { addToCartHandler: this }, this.onRemoveFromCart )
- .on( 'added_to_cart', this.updateButton )
- .on( 'ajax_request_not_sent.adding_to_cart', this.updateButton )
- .on( 'added_to_cart removed_from_cart', { addToCartHandler: this }, this.updateFragments );
+ .on( 'added_to_cart', { addToCartHandler: this }, this.onAddedToCart )
+ .on( 'removed_from_cart', { addToCartHandler: this }, this.onRemovedFromCart )
+ .on( 'ajax_request_not_sent.adding_to_cart', this.updateButton );
};
/**
@@ -65,6 +66,12 @@ jQuery( function( $ ) {
return true;
}
+ // Clean existing text in mini cart live region and update aria-relevant attribute
+ // so screen readers can identify the next update if it's the same as the previous one.
+ e.data.addToCartHandler.$liveRegion
+ .text( '' )
+ .removeAttr( 'aria-relevant' );
+
e.preventDefault();
$thisbutton.removeClass( 'added' );
@@ -127,6 +134,10 @@ jQuery( function( $ ) {
var $thisbutton = $( this ),
$row = $thisbutton.closest( '.woocommerce-mini-cart-item' );
+ e.data.addToCartHandler.$liveRegion
+ .text( '' )
+ .removeAttr( 'aria-relevant' );
+
e.preventDefault();
$row.block({
@@ -207,6 +218,55 @@ jQuery( function( $ ) {
}
};
+ /**
+ * Update cart live region message after add/remove cart events.
+ */
+ AddToCartHandler.prototype.alertCartUpdated = function( e, fragments, cart_hash, $button ) {
+ var message = $button.data( 'success_message' );
+
+ if ( !message ) {
+ return;
+ }
+
+ // If the response after adding/removing an item to/from the cart is really fast,
+ // screen readers may not have time to identify the changes in the live region element.
+ // So, we add a delay to ensure an interval between messages.
+ e.data.addToCartHandler.$liveRegion
+ .delay(1000)
+ .text( message )
+ .attr( 'aria-relevant', 'all' );
+ };
+
+ /**
+ * Add live region into the body element.
+ */
+ AddToCartHandler.prototype.createLiveRegion = function() {
+ var existingLiveRegion = $( '.widget_shopping_cart_live_region' );
+
+ if ( existingLiveRegion.length ) {
+ return existingLiveRegion;
+ }
+
+ return $( '
' ).appendTo( 'body' );
+ };
+
+ /**
+ * Callbacks after added to cart event.
+ */
+ AddToCartHandler.prototype.onAddedToCart = function( e, fragments, cart_hash, $button ) {
+ e.data.addToCartHandler.updateButton( e, fragments, cart_hash, $button );
+ e.data.addToCartHandler.updateFragments( e, fragments );
+ e.data.addToCartHandler.alertCartUpdated( e, fragments, cart_hash, $button );
+ };
+
+ /**
+ * Callbacks after removed from cart event.
+ */
+ AddToCartHandler.prototype.onRemovedFromCart = function( e, fragments, cart_hash, $button ) {
+ e.data.addToCartHandler.updateFragments( e, fragments );
+ e.data.addToCartHandler.alertCartUpdated( e, fragments, cart_hash, $button );
+ };
+
/**
* Init AddToCartHandler.
*/
diff --git a/plugins/woocommerce/client/legacy/js/frontend/address-i18n.js b/plugins/woocommerce/client/legacy/js/frontend/address-i18n.js
index f71dd266e3b..9ef9e8fc786 100644
--- a/plugins/woocommerce/client/legacy/js/frontend/address-i18n.js
+++ b/plugins/woocommerce/client/legacy/js/frontend/address-i18n.js
@@ -41,9 +41,9 @@ jQuery( function( $ ) {
thislocale = locale['default'];
}
- var $postcodefield = thisform.find( '#billing_postcode_field, #shipping_postcode_field' ),
- $cityfield = thisform.find( '#billing_city_field, #shipping_city_field' ),
- $statefield = thisform.find( '#billing_state_field, #shipping_state_field' );
+ var $postcodefield = thisform.find( '#billing_postcode_field, #shipping_postcode_field, #calc_shipping_postcode_field' ),
+ $cityfield = thisform.find( '#billing_city_field, #shipping_city_field, #calc_shipping_city_field' ),
+ $statefield = thisform.find( '#billing_state_field, #shipping_state_field, #calc_shipping_state_field' );
if ( ! $postcodefield.attr( 'data-o_class' ) ) {
$postcodefield.attr( 'data-o_class', $postcodefield.attr( 'class' ) );
@@ -74,7 +74,7 @@ jQuery( function( $ ) {
if (
typeof fieldLocale.placeholder === 'undefined' &&
typeof fieldLocale.label !== 'undefined' &&
- ! field.find( 'label' ).length
+ ! field.find( 'label:not(.screen-reader-text)' ).length
) {
field.find( ':input' ).attr( 'placeholder', fieldLocale.label );
field.find( ':input' ).attr( 'data-placeholder', fieldLocale.label );
diff --git a/plugins/woocommerce/client/legacy/js/frontend/country-select.js b/plugins/woocommerce/client/legacy/js/frontend/country-select.js
index 46a0467ff01..3260ada9ab0 100644
--- a/plugins/woocommerce/client/legacy/js/frontend/country-select.js
+++ b/plugins/woocommerce/client/legacy/js/frontend/country-select.js
@@ -60,6 +60,7 @@ jQuery( function( $ ) {
var select2_args = $.extend({
placeholder: $this.attr( 'data-placeholder' ) || $this.attr( 'placeholder' ) || '',
label: $this.attr( 'data-label' ) || null,
+ required: $this.attr( 'aria-required' ) === 'true' || null,
width: '100%'
}, getEnhancedSelectFormatString() );
@@ -104,12 +105,15 @@ jQuery( function( $ ) {
placeholder = $statebox.attr( 'placeholder' ) || $statebox.attr( 'data-placeholder' ) || '',
$newstate;
+ if ( placeholder === wc_country_select_params.i18n_select_state_text ) {
+ placeholder = '';
+ }
+
if ( states[ country ] ) {
if ( $.isEmptyObject( states[ country ] ) ) {
$newstate = $( ' ' )
.prop( 'id', input_id )
.prop( 'name', input_name )
- .prop( 'placeholder', placeholder )
.attr( 'data-input-classes', input_classes )
.addClass( 'hidden ' + input_classes );
$parent.hide().find( '.select2-container' ).remove();
@@ -154,8 +158,8 @@ jQuery( function( $ ) {
$newstate = $( ' ' )
.prop( 'id', input_id )
.prop( 'name', input_name )
- .prop('placeholder', placeholder)
- .attr('data-input-classes', input_classes )
+ .prop( 'placeholder', placeholder )
+ .attr( 'data-input-classes', input_classes )
.addClass( 'input-text ' + input_classes );
$parent.show().find( '.select2-container' ).remove();
$statebox.replaceWith( $newstate );
diff --git a/plugins/woocommerce/client/legacy/js/selectWoo/selectWoo.full.js b/plugins/woocommerce/client/legacy/js/selectWoo/selectWoo.full.js
index 82f38f68b99..3f9ab6c3ecf 100644
--- a/plugins/woocommerce/client/legacy/js/selectWoo/selectWoo.full.js
+++ b/plugins/woocommerce/client/legacy/js/selectWoo/selectWoo.full.js
@@ -1392,9 +1392,14 @@ S2.define('select2/selection/base',[
var id = container.id + '-container';
var resultsId = container.id + '-results';
var searchHidden = this.options.get('minimumResultsForSearch') === Infinity;
+ var isRequired = this.options.get('required') === true;
this.container = container;
+ if (isRequired) {
+ this.$selection.attr('aria-required', 'true')
+ }
+
this.$selection.on('focus', function (evt) {
self.trigger('focus', evt);
});
@@ -1553,6 +1558,11 @@ S2.define('select2/selection/single',[
var id = container.id + '-container';
+ var isRequired = this.options.get('required') === true;
+ if (isRequired) {
+ this.$selection.find('.select2-selection__rendered').attr('aria-required', 'true')
+ }
+
this.$selection.find('.select2-selection__rendered')
.attr('id', id)
.attr('role', 'textbox')
@@ -5069,6 +5079,10 @@ S2.define('select2/options',[
this.options.disabled = $e.prop('disabled');
}
+ if (!this.options.required) {
+ this.options.required = $e.prop('required');
+ }
+
if (this.options.language == null) {
if ($e.prop('lang')) {
this.options.language = $e.prop('lang').toLowerCase();
diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json
index 80809c5668c..41621b29fb3 100644
--- a/plugins/woocommerce/composer.json
+++ b/plugins/woocommerce/composer.json
@@ -2,7 +2,7 @@
"name": "woocommerce/woocommerce",
"description": "An eCommerce toolkit that helps you sell anything. Beautifully.",
"homepage": "https://woocommerce.com/",
- "version": "9.2.0",
+ "version": "9.3.0",
"type": "wordpress-plugin",
"license": "GPL-3.0-or-later",
"prefer-stable": true,
@@ -19,6 +19,7 @@
],
"require": {
"php": ">=7.4",
+ "automattic/jetpack-a8c-mc-stats": "^1.4",
"automattic/jetpack-autoloader": "2.11.18",
"automattic/jetpack-config": "1.15.2",
"automattic/jetpack-connection": "^1.57",
@@ -26,7 +27,7 @@
"composer/installers": "^1.9",
"maxmind-db/reader": "^1.11",
"pelago/emogrifier": "^6.0",
- "woocommerce/action-scheduler": "3.7.4"
+ "woocommerce/action-scheduler": "3.8.1"
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.0",
diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock
index 51cdcf7df3e..6c2e7b4e819 100644
--- a/plugins/woocommerce/composer.lock
+++ b/plugins/woocommerce/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "12ea33bcea6a641c43d24712adc9f0b6",
+ "content-hash": "37a17bfeee7e4517f8169fc4c0754d86",
"packages": [
{
"name": "automattic/jetpack-a8c-mc-stats",
@@ -960,16 +960,16 @@
},
{
"name": "woocommerce/action-scheduler",
- "version": "3.7.4",
+ "version": "3.8.1",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/action-scheduler.git",
- "reference": "5fb655253dc004bb7a6d840da807f0949aea8bcd"
+ "reference": "e331b534d7de10402d7545a0de50177b874c0779"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/5fb655253dc004bb7a6d840da807f0949aea8bcd",
- "reference": "5fb655253dc004bb7a6d840da807f0949aea8bcd",
+ "url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/e331b534d7de10402d7545a0de50177b874c0779",
+ "reference": "e331b534d7de10402d7545a0de50177b874c0779",
"shasum": ""
},
"require": {
@@ -997,9 +997,9 @@
"homepage": "https://actionscheduler.org/",
"support": {
"issues": "https://github.com/woocommerce/action-scheduler/issues",
- "source": "https://github.com/woocommerce/action-scheduler/tree/3.7.4"
+ "source": "https://github.com/woocommerce/action-scheduler/tree/3.8.1"
},
- "time": "2024-04-05T14:42:07+00:00"
+ "time": "2024-06-20T19:53:06+00:00"
}
],
"packages-dev": [
@@ -4620,7 +4620,7 @@
},
{
"name": "woocommerce/monorepo-plugin",
- "version": "dev-update/action-scheduler-3.7.4",
+ "version": "dev-trunk",
"dist": {
"type": "path",
"url": "../../packages/php/monorepo-plugin",
@@ -4776,5 +4776,5 @@
"platform-overrides": {
"php": "7.4"
},
- "plugin-api-version": "2.3.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
index 99a45d0c339..ac30221490b 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
@@ -626,7 +626,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
*/
public function set_status( $new_status ) {
$old_status = $this->get_status();
- $new_status = 'wc-' === substr( $new_status, 0, 3 ) ? substr( $new_status, 3 ) : $new_status;
+ $new_status = OrderUtil::remove_status_prefix( $new_status );
$status_exceptions = array( 'auto-draft', 'trash' );
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
index 6e85be8b839..e32e6e2d735 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
@@ -860,7 +860,7 @@ class WC_Product extends WC_Abstract_Legacy_Product {
$this->error(
'product_invalid_global_unique_id',
- __( 'Invalid or duplicated Unique ID.', 'woocommerce' ),
+ __( 'Invalid or duplicated GTIN, UPC, EAN or ISBN.', 'woocommerce' ),
400,
array(
'resource_id' => $global_unique_id_found,
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
index ef3c34e4b51..6ceb6c82f77 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
@@ -276,7 +276,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
if ( in_array( $screen_id, array( 'product', 'edit-product' ) ) ) {
wp_enqueue_media();
wp_register_script( 'wc-admin-product-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'media-models' ), $version );
- wp_register_script( 'wc-admin-variation-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product-variation' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'serializejson', 'media-models', 'backbone', 'jquery-ui-sortable', 'wc-backbone-modal' ), $version );
+ wp_register_script( 'wc-admin-variation-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product-variation' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'serializejson', 'media-models', 'backbone', 'jquery-ui-sortable', 'wc-backbone-modal', 'wp-data', 'wp-notices' ), $version );
wp_enqueue_script( 'wc-admin-product-meta-boxes' );
wp_enqueue_script( 'wc-admin-variation-meta-boxes' );
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-profile.php b/plugins/woocommerce/includes/admin/class-wc-admin-profile.php
index cf1b0d042e3..90f2d52da82 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-profile.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-profile.php
@@ -176,7 +176,7 @@ if ( ! class_exists( 'WC_Admin_Profile', false ) ) :
-
+
ID, $key, true ) );
foreach ( $field['options'] as $option_key => $option_value ) :
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
index a5f0de69566..d2458143813 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
@@ -53,7 +53,11 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
$settings[] = include __DIR__ . '/settings/class-wc-settings-products.php';
$settings[] = include __DIR__ . '/settings/class-wc-settings-tax.php';
$settings[] = include __DIR__ . '/settings/class-wc-settings-shipping.php';
- $settings[] = include __DIR__ . '/settings/class-wc-settings-payment-gateways.php';
+ if ( \Automattic\WooCommerce\Admin\Features\Features::is_enabled( 'reactify-classic-payments-settings' ) ) {
+ $settings[] = include __DIR__ . '/settings/class-wc-settings-payment-gateways-react.php';
+ } else {
+ $settings[] = include __DIR__ . '/settings/class-wc-settings-payment-gateways.php';
+ }
$settings[] = include __DIR__ . '/settings/class-wc-settings-accounts.php';
$settings[] = include __DIR__ . '/settings/class-wc-settings-emails.php';
$settings[] = include __DIR__ . '/settings/class-wc-settings-integrations.php';
diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php
index 8db27dbbc78..cb83e7064aa 100644
--- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php
+++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php
@@ -27,7 +27,19 @@ class WC_Helper_Admin {
* @return void
*/
public static function load() {
- add_filter( 'woocommerce_admin_shared_settings', array( __CLASS__, 'add_marketplace_settings' ) );
+ global $pagenow;
+
+ if ( is_admin() ) {
+ $is_in_app_marketplace = ( 'admin.php' === $pagenow
+ && isset( $_GET['page'] ) && 'wc-admin' === $_GET['page'] //phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ && isset( $_GET['path'] ) && '/extensions' === $_GET['path'] //phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ );
+
+ if ( $is_in_app_marketplace ) {
+ add_filter( 'woocommerce_admin_shared_settings', array( __CLASS__, 'add_marketplace_settings' ) );
+ }
+ }
+
add_filter( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
}
diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php
index 9c590497005..8a0822e8d7a 100644
--- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php
+++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php
@@ -6,6 +6,8 @@
* @package WooCommerce\Admin\Helper
*/
+use Automattic\WooCommerce\Admin\PluginsHelper;
+
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -252,41 +254,68 @@ class WC_Helper_Updater {
* @return void.
*/
public static function display_notice_for_expired_and_expiring_subscriptions( $plugin_data, $response ) {
-
// Extract product ID from the response.
$product_id = preg_replace( '/[^0-9]/', '', $response->id );
- // Get the subscription details based on product ID.
- $subscription = current(
- wp_list_filter(
- WC_Helper::get_subscriptions(),
- array( 'product_id' => $product_id )
- )
- );
-
- // Check if subscription is empty.
- if ( empty( $subscription ) ) {
+ // Product subscriptions.
+ $subscriptions = wp_list_filter( WC_Helper::get_installed_subscriptions(), array( 'product_id' => $product_id ) );
+ if ( empty( $subscriptions ) ) {
return;
}
+ $expired_subscription = current(
+ array_filter(
+ $subscriptions,
+ function ( $subscription ) {
+ return ! empty( $subscription['expired'] ) && ! $subscription['lifetime'];
+ }
+ )
+ );
+
+ $expiring_subscription = current(
+ array_filter(
+ $subscriptions,
+ function ( $subscription ) {
+ return ! empty( $subscription['expiring'] ) && ! $subscription['autorenew'];
+ }
+ )
+ );
+
// Prepare the expiry notice based on subscription status.
$expiry_notice = '';
- if ( ! empty( $subscription['expired'] ) && ! $subscription['lifetime'] ) {
+ if ( ! empty( $expired_subscription ) ) {
+
+ $renew_link = add_query_arg(
+ array(
+ 'utm_source' => 'pu',
+ 'utm_campaign' => 'pu_plugin_screen_renew',
+ ),
+ PluginsHelper::WOO_SUBSCRIPTION_PAGE_URL
+ );
+
/* translators: 1: Product regular price */
- $product_price = ! empty( $subscription['product_regular_price'] ) ? sprintf( __( 'for %s ', 'woocommerce' ), esc_html( $subscription['product_regular_price'] ) ) : '';
+ $product_price = ! empty( $expired_subscription['product_regular_price'] ) ? sprintf( __( 'for %s ', 'woocommerce' ), esc_html( $expired_subscription['product_regular_price'] ) ) : '';
$expiry_notice = sprintf(
/* translators: 1: URL to My Subscriptions page 2: Product price */
__( ' Your subscription expired, renew %2$s to update.', 'woocommerce' ),
- esc_url( 'https://woocommerce.com/my-account/my-subscriptions/' ),
+ esc_url( $renew_link ),
$product_price
);
- } elseif ( ! empty( $subscription['expiring'] ) && ! $subscription['autorenew'] ) {
+ } elseif ( ! empty( $expiring_subscription ) ) {
+ $renew_link = add_query_arg(
+ array(
+ 'utm_source' => 'pu',
+ 'utm_campaign' => 'pu_plugin_screen_enable_autorenew',
+ ),
+ PluginsHelper::WOO_SUBSCRIPTION_PAGE_URL
+ );
+
$expiry_notice = sprintf(
/* translators: 1: Expiry date 1: URL to My Subscriptions page */
__( ' Your subscription expires on %1$s, enable auto-renew to continue receiving updates.', 'woocommerce' ),
- date_i18n( 'F jS', $subscription['expires'] ),
- esc_url( 'https://woocommerce.com/my-account/my-subscriptions/' )
+ date_i18n( 'F jS', $expiring_subscription['expires'] ),
+ esc_url( $renew_link )
);
}
diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php
index e35057b4c6b..13593508bf4 100644
--- a/plugins/woocommerce/includes/admin/helper/class-wc-helper.php
+++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php
@@ -63,6 +63,7 @@ class WC_Helper {
include_once __DIR__ . '/class-wc-helper-admin.php';
include_once __DIR__ . '/class-wc-helper-subscriptions-api.php';
include_once __DIR__ . '/class-wc-helper-orders-api.php';
+ include_once __DIR__ . '/class-wc-product-usage-notice.php';
}
/**
@@ -1296,6 +1297,60 @@ class WC_Helper {
return ! empty( $subscription );
}
+ /**
+ * Get the user's connected subscriptions that are installed on the current
+ * site.
+ *
+ * @return array
+ */
+ public static function get_installed_subscriptions() {
+ static $installed_subscriptions = null;
+
+ // Cache installed_subscriptions in the current request.
+ if ( is_null( $installed_subscriptions ) ) {
+ $auth = WC_Helper_Options::get( 'auth' );
+ $site_id = isset( $auth['site_id'] ) ? absint( $auth['site_id'] ) : 0;
+ if ( 0 === $site_id ) {
+ $installed_subscriptions = array();
+ return $installed_subscriptions;
+ }
+
+ $installed_subscriptions = array_filter(
+ self::get_subscriptions(),
+ function ( $subscription ) use ( $site_id ) {
+ return in_array( $site_id, $subscription['connections'], true );
+ }
+ );
+ }
+
+ return $installed_subscriptions;
+ }
+
+ /**
+ * Get subscription state of a given product ID.
+ *
+ * @since TBD
+ *
+ * @param int $product_id The product id.
+ *
+ * @return array Array of state_name => (bool) state
+ */
+ public static function get_product_subscription_state( $product_id ) {
+ $product_subscriptions = wp_list_filter( self::get_installed_subscriptions(), array( 'product_id' => $product_id ) );
+
+ $subscription = ! empty( $product_subscriptions )
+ ? array_shift( $product_subscriptions )
+ : array();
+
+ return array(
+ 'unregistered' => empty( $subscription ),
+ 'expired' => ( isset( $subscription['expired'] ) && $subscription['expired'] ),
+ 'expiring' => ( isset( $subscription['expiring'] ) && $subscription['expiring'] ),
+ 'key' => $subscription['product_key'] ?? '',
+ 'order_id' => $subscription['order_id'] ?? '',
+ );
+ }
+
/**
* Get a subscription entry from product_id. If multiple subscriptions are
* found with the same product id and $single is set to true, will return the
@@ -1482,6 +1537,41 @@ class WC_Helper {
return $woo_themes;
}
+ /**
+ * Get rules for displaying notice regarding marketplace product usage.
+ *
+ * @return array
+ */
+ public static function get_product_usage_notice_rules() {
+ $cache_key = '_woocommerce_helper_product_usage_notice_rules';
+ $data = get_transient( $cache_key );
+ if ( false !== $data ) {
+ return $data;
+ }
+
+ $request = WC_Helper_API::get(
+ 'product-usage-notice-rules',
+ array(
+ 'authenticated' => false,
+ )
+ );
+
+ // Retry in 15 minutes for non-200 response.
+ if ( wp_remote_retrieve_response_code( $request ) !== 200 ) {
+ set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS );
+ return array();
+ }
+
+ $data = json_decode( wp_remote_retrieve_body( $request ), true );
+ if ( empty( $data ) || ! is_array( $data ) ) {
+ $data = array();
+ }
+
+ set_transient( $cache_key, $data, 1 * HOUR_IN_SECONDS );
+ return $data;
+ }
+
+
/**
* Get the connected user's subscriptions.
*
diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-product-usage-notice.php b/plugins/woocommerce/includes/admin/helper/class-wc-product-usage-notice.php
new file mode 100644
index 00000000000..ba24b42c7fd
--- /dev/null
+++ b/plugins/woocommerce/includes/admin/helper/class-wc-product-usage-notice.php
@@ -0,0 +1,384 @@
+= $max_dismissals;
+ }
+
+ /**
+ * Check whether the user dismissed any product usage notices recently.
+ *
+ * @param int $user_id User ID.
+ *
+ * @return bool
+ */
+ private static function is_any_notices_dismissed_recently( int $user_id ): bool {
+ $global_last_dismissed_ts = absint(
+ get_user_meta(
+ $user_id,
+ self::LAST_DISMISSED_TIMESTAMP_META,
+ true
+ )
+ );
+ if ( 0 === $global_last_dismissed_ts ) {
+ return false;
+ }
+
+ $seconds_since_dismissed = time() - $global_last_dismissed_ts;
+
+ $wait_after_any_dismisses = self::$product_usage_notice_rules['wait_in_seconds_after_any_dismisses'];
+
+ return $seconds_since_dismissed < $wait_after_any_dismisses;
+ }
+
+ /**
+ * Check whether the user dismissed given product usage notice recently.
+ *
+ * @param int $user_id User ID.
+ * @param int $product_id Product ID.
+ *
+ * @return bool
+ */
+ private static function is_product_notice_dismissed_recently( int $user_id, int $product_id ): bool {
+ $last_dismissed_ts = absint(
+ get_user_meta(
+ $user_id,
+ self::DISMISSED_TIMESTAMP_META_PREFIX . $product_id,
+ true
+ )
+ );
+ if ( 0 === $last_dismissed_ts ) {
+ return false;
+ }
+
+ $seconds_since_dismissed = time() - $last_dismissed_ts;
+
+ $wait_after_dismiss = self::$current_notice_rule['wait_in_seconds_after_dismiss'];
+
+ return $seconds_since_dismissed < $wait_after_dismiss;
+ }
+
+ /**
+ * Check whether current notice is throttled for the user and product.
+ *
+ * @param int $user_id User ID.
+ * @param int $product_id Product ID.
+ *
+ * @return bool
+ */
+ private static function is_notice_throttled( int $user_id, int $product_id ): bool {
+ return self::is_remind_later_clicked_recently( $user_id, $product_id ) ||
+ self::has_reached_max_dismissals( $user_id, $product_id ) ||
+ self::is_any_notices_dismissed_recently( $user_id ) ||
+ self::is_product_notice_dismissed_recently( $user_id, $product_id );
+ }
+
+ /**
+ * Enqueue scripts needed to display product usage notice (or modal).
+ */
+ public static function enqueue_product_usage_notice_scripts() {
+ WCAdminAssets::register_style( 'woo-product-usage-notice', 'style', array( 'wp-components' ) );
+ WCAdminAssets::register_script( 'wp-admin-scripts', 'woo-product-usage-notice', true );
+
+ $subscribe_url = add_query_arg(
+ array(
+ 'add-to-cart' => self::$current_notice_rule['id'],
+ 'utm_source' => 'pu',
+ 'utm_medium' => 'product',
+ 'utm_campaign' => 'pu_modal_subscribe',
+ ),
+ 'https://woocommerce.com/cart/'
+ );
+
+ $renew_url = add_query_arg(
+ array(
+ 'renew_product' => self::$current_notice_rule['id'],
+ 'product_key' => self::$current_notice_rule['state']['key'],
+ 'order_id' => self::$current_notice_rule['state']['order_id'],
+ 'utm_source' => 'pu',
+ 'utm_medium' => 'product',
+ 'utm_campaign' => 'pu_modal_renew',
+ ),
+ 'https://woocommerce.com/cart/'
+ );
+
+ wp_localize_script(
+ 'wc-admin-woo-product-usage-notice',
+ 'wooProductUsageNotice',
+ array(
+ 'subscribeUrl' => $subscribe_url,
+ 'renewUrl' => $renew_url,
+ 'dismissAction' => 'woocommerce_dismiss_product_usage_notice',
+ 'remindLaterAction' => 'woocommerce_remind_later_product_usage_notice',
+ 'productId' => self::$current_notice_rule['id'],
+ 'productName' => self::$current_notice_rule['name'],
+ 'productRegularPrice' => self::$current_notice_rule['regular_price'],
+ 'dismissNonce' => wp_create_nonce( 'dismiss_product_usage_notice' ),
+ 'remindLaterNonce' => wp_create_nonce( 'remind_later_product_usage_notice' ),
+ 'showAs' => self::$current_notice_rule['show_as'],
+ 'colorScheme' => self::$current_notice_rule['color_scheme'],
+ 'subscriptionState' => self::$current_notice_rule['state'],
+ 'screenId' => get_current_screen()->id,
+ )
+ );
+ }
+
+ /**
+ * Get product usage notice rule from a given WP_Screen object.
+ *
+ * @param \WP_Screen $screen Current \WP_Screen object.
+ *
+ * @return array
+ */
+ private static function get_current_notice_rule( $screen ) {
+ foreach ( self::$product_usage_notice_rules['products'] as $product_id => $rule ) {
+ if ( ! isset( $rule['screens'][ $screen->id ] ) ) {
+ continue;
+ }
+
+ // Check query strings.
+ if ( ! self::query_string_matches( $screen, $rule ) ) {
+ continue;
+ }
+
+ $product_id = absint( $product_id );
+ $state = WC_Helper::get_product_subscription_state( $product_id );
+ if ( $state['expired'] || $state['unregistered'] ) {
+ $rule['id'] = $product_id;
+ $rule['state'] = $state;
+ return $rule;
+ }
+ }
+
+ return array();
+ }
+
+ /**
+ * Check whether the screen and GET parameter matches a given rule.
+ *
+ * @param \WP_Screen $screen Current \WP_Screen object.
+ * @param array $rule Product usage notice rule.
+ *
+ * @return bool
+ */
+ private static function query_string_matches( $screen, $rule ) {
+ if ( empty( $rule['screens'][ $screen->id ]['qs'] ) ) {
+ return true;
+ }
+
+ $qs = $rule['screens'][ $screen->id ]['qs'];
+ foreach ( $qs as $key => $val ) {
+ // phpcs:disable WordPress.Security.NonceVerification.Recommended
+ if ( empty( $_GET[ $key ] ) || $_GET[ $key ] !== $val ) {
+ return false;
+ }
+ // phpcs:enable WordPress.Security.NonceVerification.Recommended
+ }
+ return true;
+ }
+
+ /**
+ * AJAX handler for dismiss action of product usage notice.
+ */
+ public static function ajax_dismiss() {
+ if ( ! check_ajax_referer( 'dismiss_product_usage_notice' ) ) {
+ wp_die( -1 );
+ }
+
+ $user_id = get_current_user_id();
+ if ( ! $user_id ) {
+ wp_die( -1 );
+ }
+
+ $product_id = absint( $_GET['product_id'] ?? 0 );
+ if ( ! $product_id ) {
+ wp_die( -1 );
+ }
+
+ $dismiss_count = absint( get_user_meta( $user_id, self::DISMISSED_COUNT_META_PREFIX . $product_id, true ) );
+ update_user_meta( $user_id, self::DISMISSED_COUNT_META_PREFIX . $product_id, $dismiss_count + 1 );
+
+ update_user_meta( $user_id, self::DISMISSED_TIMESTAMP_META_PREFIX . $product_id, time() );
+ update_user_meta( $user_id, self::LAST_DISMISSED_TIMESTAMP_META, time() );
+
+ wp_die( 1 );
+ }
+
+ /**
+ * AJAX handler for "remind later" action of product usage notice.
+ */
+ public static function ajax_remind_later() {
+ if ( ! check_ajax_referer( 'remind_later_product_usage_notice' ) ) {
+ wp_die( -1 );
+ }
+
+ $user_id = get_current_user_id();
+ if ( ! $user_id ) {
+ wp_die( -1 );
+ }
+
+ $product_id = absint( $_GET['product_id'] ?? 0 );
+ if ( ! $product_id ) {
+ wp_die( -1 );
+ }
+
+ update_user_meta( $user_id, self::REMIND_LATER_TIMESTAMP_META_PREFIX . $product_id, time() );
+
+ wp_die( 1 );
+ }
+}
+
+WC_Product_Usage_Notice::load();
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php
index 5b51b6f7673..d285fdb1b85 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-data.php
@@ -562,7 +562,7 @@ class WC_Meta_Box_Order_Data {
}
if ( apply_filters( 'woocommerce_enable_order_notes_field', 'yes' === get_option( 'woocommerce_enable_order_comments', 'yes' ) ) && $order->get_customer_note() ) { // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
- echo '' . esc_html( __( 'Customer provided note:', 'woocommerce' ) ) . ' ' . nl2br( esc_html( $order->get_customer_note() ) ) . '
';
+ echo '' . esc_html( __( 'Customer provided note:', 'woocommerce' ) ) . ' ' . wp_kses( nl2br( esc_html( $order->get_customer_note() ) ), array() ) . '
';
}
?>
@@ -615,7 +615,7 @@ class WC_Meta_Box_Order_Data {
?>
:
-
+
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php
index 47d51af710c..5a7c55fdb3b 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php
@@ -369,6 +369,7 @@ class WC_Meta_Box_Product_Data {
$errors = $product->set_props(
array(
'sku' => isset( $_POST['_sku'] ) ? wc_clean( wp_unslash( $_POST['_sku'] ) ) : null,
+ 'global_unique_id' => isset( $_POST['_global_unique_id'] ) ? wc_clean( wp_unslash( $_POST['_global_unique_id'] ) ) : null,
'purchase_note' => isset( $_POST['_purchase_note'] ) ? wp_kses_post( wp_unslash( $_POST['_purchase_note'] ) ) : '',
'downloadable' => isset( $_POST['_downloadable'] ),
'virtual' => isset( $_POST['_virtual'] ),
@@ -544,6 +545,7 @@ class WC_Meta_Box_Product_Data {
'image_id' => isset( $_POST['upload_image_id'][ $i ] ) ? wc_clean( wp_unslash( $_POST['upload_image_id'][ $i ] ) ) : null,
'attributes' => self::prepare_set_attributes( $parent->get_attributes(), 'attribute_', $i ),
'sku' => isset( $_POST['variable_sku'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_sku'][ $i ] ) ) : '',
+ 'global_unique_id' => isset( $_POST['variable_global_unique_id'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_global_unique_id'][ $i ] ) ) : '',
'weight' => isset( $_POST['variable_weight'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_weight'][ $i ] ) ) : '',
'length' => isset( $_POST['variable_length'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_length'][ $i ] ) ) : '',
'width' => isset( $_POST['variable_width'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_width'][ $i ] ) ) : '',
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-fee.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-fee.php
index 701a3b8c309..79c0236b2f5 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-fee.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-fee.php
@@ -78,7 +78,7 @@ if ( ! defined( 'ABSPATH' ) ) {
is_editable() ) : ?>
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-item.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-item.php
index 13c301748e7..8e1e463c07f 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-item.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-item.php
@@ -181,7 +181,7 @@ $row_class = apply_filters( 'woocommerce_admin_html_order_item_class', ! empt
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-shipping.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-shipping.php
index ade41c05ef8..a69e06c47c1 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-shipping.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-shipping.php
@@ -109,7 +109,7 @@ if ( ! defined( 'ABSPATH' ) ) {
is_editable() ) : ?>
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-inventory.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-inventory.php
index e14605de6cc..2900c929e88 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-inventory.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-inventory.php
@@ -28,6 +28,19 @@ if ( ! defined( 'ABSPATH' ) ) {
do_action( 'woocommerce_product_options_sku' );
+ woocommerce_wp_text_input(
+ array(
+ 'id' => '_global_unique_id',
+ 'value' => $product_object->get_global_unique_id( 'edit' ),
+ // translators: %1$s GTIN %2$s UPC %3$s EAN %4$s ISBN.
+ 'label' => sprintf( __( '%1$s, %2$s, %3$s, or %4$s', 'woocommerce' ), '' . esc_html__( 'GTIN', 'woocommerce' ) . ' ', '' . esc_html__( 'UPC', 'woocommerce' ) . ' ', '' . esc_html__( 'EAN', 'woocommerce' ) . ' ', '' . esc_html__( 'ISBN', 'woocommerce' ) . ' ' ),
+ 'desc_tip' => true,
+ 'description' => __( 'Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.', 'woocommerce' ),
+ )
+ );
+
+ do_action( 'woocommerce_product_options_global_unique_id' );
+
?>
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php
index 2ff898c6bbf..9a205f574e3 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php
@@ -70,27 +70,44 @@ defined( 'ABSPATH' ) || exit;
-
-
-
-
-
-
+
+
+
+
+
+
+ "variable_sku{$loop}",
+ 'name' => "variable_sku[{$loop}]",
+ 'value' => $variation_object->get_sku( 'edit' ),
+ 'placeholder' => $variation_object->get_sku(),
+ 'label' => '
' . esc_html__( 'SKU', 'woocommerce' ) . ' ',
+ 'desc_tip' => true,
+ 'description' => __( 'SKU refers to a Stock-keeping unit, a unique identifier for each distinct product and service that can be purchased.', 'woocommerce' ),
+ 'wrapper_class' => 'form-row',
+ )
+ );
+ }
woocommerce_wp_text_input(
array(
- 'id' => "variable_sku{$loop}",
- 'name' => "variable_sku[{$loop}]",
- 'value' => $variation_object->get_sku( 'edit' ),
- 'placeholder' => $variation_object->get_sku(),
- 'label' => '
' . esc_html__( 'SKU', 'woocommerce' ) . ' ',
+ 'id' => "variable_global_unique_id{$loop}",
+ 'name' => "variable_global_unique_id[{$loop}]",
+ 'value' => $variation_object->get_global_unique_id( 'edit' ),
+ 'placeholder' => $variation_object->get_global_unique_id(),
+ // translators: %1$s GTIN %2$s UPC %3$s EAN %4$s ISBN.
+ 'label' => sprintf( __( '%1$s, %2$s, %3$s, or %4$s', 'woocommerce' ), '
' . esc_html__( 'GTIN', 'woocommerce' ) . ' ', '
' . esc_html__( 'UPC', 'woocommerce' ) . ' ', '
' . esc_html__( 'EAN', 'woocommerce' ) . ' ', '
' . esc_html__( 'ISBN', 'woocommerce' ) . ' ' ),
'desc_tip' => true,
- 'description' => __( 'SKU refers to a Stock-keeping unit, a unique identifier for each distinct product and service that can be purchased.', 'woocommerce' ),
- 'wrapper_class' => 'form-row form-row-last',
+ 'description' => __( 'Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.', 'woocommerce' ),
+ 'wrapper_class' => 'form-row',
)
);
- }
- ?>
+ ?>
+
+
diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-accounts.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-accounts.php
index b8c18059943..49cd3ade06e 100644
--- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-accounts.php
+++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-accounts.php
@@ -11,6 +11,8 @@ if ( class_exists( 'WC_Settings_Accounts', false ) ) {
return new WC_Settings_Accounts();
}
+use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
+
/**
* WC_Settings_Accounts.
*/
@@ -237,6 +239,25 @@ class WC_Settings_Accounts extends WC_Settings_Page {
),
);
+ // Change settings when using the block based checkout.
+ if ( CartCheckoutUtils::is_checkout_block_default() ) {
+ $account_settings = array_filter(
+ $account_settings,
+ function ( $setting ) {
+ return 'woocommerce_registration_generate_username' !== $setting['id'];
+ },
+ );
+ $account_settings = array_map(
+ function ( $setting ) {
+ if ( 'woocommerce_registration_generate_password' === $setting['id'] ) {
+ unset( $setting['checkboxgroup'] );
+ }
+ return $setting;
+ },
+ $account_settings
+ );
+ }
+
/**
* Filter account settings.
*
@@ -270,12 +291,15 @@ class WC_Settings_Accounts extends WC_Settings_Page {
function updateInputs() {
const isChecked = checkboxes.some(cb => cb && cb.checked);
inputs.forEach(input => {
+ if ( ! input ) {
+ return;
+ }
input.disabled = !isChecked;
input.closest('td').classList.toggle("disabled", !isChecked);
});
}
- checkboxes.forEach(cb => cb.addEventListener('change', updateInputs));
+ checkboxes.forEach(cb => cb && cb.addEventListener('change', updateInputs));
updateInputs(); // Initial state
});
diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways-react.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways-react.php
new file mode 100644
index 00000000000..178c33a4daf
--- /dev/null
+++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways-react.php
@@ -0,0 +1,172 @@
+id = 'checkout';
+ $this->label = _x( 'Payments', 'Settings tab label', 'woocommerce' );
+ parent::__construct();
+ }
+
+ /**
+ * Output the settings.
+ */
+ public function output() {
+ //phpcs:disable WordPress.Security.NonceVerification.Recommended
+ global $current_section;
+
+ // Load gateways so we can show any global options they may have.
+ $payment_gateways = WC()->payment_gateways->payment_gateways();
+
+ if ( $this->should_render_react_section( $current_section ) ) {
+ $this->render_react_section( $current_section );
+ } elseif ( $current_section ) {
+ $this->render_classic_gateway_settings_page( $payment_gateways, $current_section );
+ } else {
+ $this->render_react_section( 'main' );
+ }
+
+ parent::output();
+ //phpcs:enable
+ }
+
+ /**
+ * Check if the given section should be rendered using React.
+ *
+ * @param string $section The section to check.
+ * @return bool Whether the section should be rendered using React.
+ */
+ private function should_render_react_section( $section ) {
+ return in_array( $section, $this->get_reactify_render_sections(), true );
+ }
+
+ /**
+ * Render the React section.
+ *
+ * @param string $section The section to render.
+ */
+ private function render_react_section( $section ) {
+ global $hide_save_button;
+ $hide_save_button = true;
+ echo '
';
+ }
+
+ /**
+ * Render the classic gateway settings page.
+ *
+ * @param array $payment_gateways The payment gateways.
+ * @param string $current_section The current section.
+ */
+ private function render_classic_gateway_settings_page( $payment_gateways, $current_section ) {
+ foreach ( $payment_gateways as $gateway ) {
+ if ( in_array( $current_section, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ), true ) ) {
+ if ( isset( $_GET['toggle_enabled'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $enabled = $gateway->get_option( 'enabled' );
+
+ if ( $enabled ) {
+ $gateway->settings['enabled'] = wc_string_to_bool( $enabled ) ? 'no' : 'yes';
+ }
+ }
+ $this->run_gateway_admin_options( $gateway );
+ break;
+ }
+ }
+ }
+
+ /**
+ * Run the 'admin_options' method on a given gateway.
+ * This method exists to easy unit testing.
+ *
+ * @param object $gateway The gateway object to run the method on.
+ */
+ protected function run_gateway_admin_options( $gateway ) {
+ $gateway->admin_options();
+ }
+
+ /**
+ * Don't show any section links.
+ *
+ * @return array
+ */
+ public function get_sections() {
+ return array();
+ }
+
+ /**
+ * Save settings.
+ */
+ public function save() {
+ global $current_section;
+
+ $wc_payment_gateways = WC_Payment_Gateways::instance();
+
+ $this->save_settings_for_current_section();
+
+ if ( ! $current_section ) {
+ // If section is empty, we're on the main settings page. This makes sure 'gateway ordering' is saved.
+ $wc_payment_gateways->process_admin_options();
+ $wc_payment_gateways->init();
+ } else {
+ // There is a section - this may be a gateway or custom section.
+ foreach ( $wc_payment_gateways->payment_gateways() as $gateway ) {
+ if ( in_array( $current_section, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ), true ) ) {
+ /**
+ * Fires update actions for payment gateways.
+ *
+ * @since 3.4.0
+ *
+ * @param int $gateway->id Gateway ID.
+ */
+ do_action( 'woocommerce_update_options_payment_gateways_' . $gateway->id );
+ $wc_payment_gateways->init();
+ }
+ }
+
+ $this->do_update_options_action();
+ }
+ }
+}
+
+return new WC_Settings_Payment_Gateways_React();
diff --git a/plugins/woocommerce/includes/admin/settings/views/html-admin-page-shipping-zones.php b/plugins/woocommerce/includes/admin/settings/views/html-admin-page-shipping-zones.php
index 980a9806f44..49329da8921 100644
--- a/plugins/woocommerce/includes/admin/settings/views/html-admin-page-shipping-zones.php
+++ b/plugins/woocommerce/includes/admin/settings/views/html-admin-page-shipping-zones.php
@@ -8,7 +8,20 @@ if ( ! defined( 'ABSPATH' ) ) {
-
+
+ local pickup settings.",
+ 'woocommerce'
+ ),
+ esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=pickup_location' ) )
+ )
+ );
+ ?>
+
@@ -56,14 +69,13 @@ if ( ! defined( 'ABSPATH' ) ) {
-
-
+
+
-
+
@@ -95,9 +107,9 @@ if ( ! defined( 'ABSPATH' ) ) {
diff --git a/plugins/woocommerce/includes/class-wc-auth.php b/plugins/woocommerce/includes/class-wc-auth.php
index 099282c7a62..9614234b53f 100644
--- a/plugins/woocommerce/includes/class-wc-auth.php
+++ b/plugins/woocommerce/includes/class-wc-auth.php
@@ -333,7 +333,7 @@ class WC_Auth {
*/
// Check if Jetpack is installed and activated.
- if ( class_exists( 'Jetpack' ) && Jetpack::connection()->is_active() ) {
+ if ( class_exists( 'Jetpack' ) && Jetpack::connection()->has_connected_owner() ) {
// Check if the user is using the WordPress.com SSO.
if ( Jetpack::is_module_active( 'sso' ) ) {
@@ -341,7 +341,7 @@ class WC_Auth {
$redirect_url = $this->build_url( $data, 'authorize' );
// Build the SSO URL.
- $login_url = Jetpack_SSO::get_instance()->build_sso_button_url(
+ $login_url = \Automattic\Jetpack\Connection\SSO::get_instance()->build_sso_button_url(
array(
'redirect_to' => rawurlencode( esc_url_raw( $redirect_url ) ),
'action' => 'login',
diff --git a/plugins/woocommerce/includes/class-wc-cart.php b/plugins/woocommerce/includes/class-wc-cart.php
index b3d2816f442..6b036136f8a 100644
--- a/plugins/woocommerce/includes/class-wc-cart.php
+++ b/plugins/woocommerce/includes/class-wc-cart.php
@@ -106,6 +106,7 @@ class WC_Cart extends WC_Legacy_Cart {
add_action( 'woocommerce_add_to_cart', array( $this, 'calculate_totals' ), 20, 0 );
add_action( 'woocommerce_applied_coupon', array( $this, 'calculate_totals' ), 20, 0 );
+ add_action( 'woocommerce_removed_coupon', array( $this, 'calculate_totals' ), 20, 0 );
add_action( 'woocommerce_cart_item_removed', array( $this, 'calculate_totals' ), 20, 0 );
add_action( 'woocommerce_cart_item_restored', array( $this, 'calculate_totals' ), 20, 0 );
add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_items' ), 1 );
@@ -715,7 +716,6 @@ class WC_Cart extends WC_Legacy_Cart {
}
return $return;
-
}
/**
diff --git a/plugins/woocommerce/includes/class-wc-cli.php b/plugins/woocommerce/includes/class-wc-cli.php
index 274704e683c..c30255ac558 100644
--- a/plugins/woocommerce/includes/class-wc-cli.php
+++ b/plugins/woocommerce/includes/class-wc-cli.php
@@ -6,7 +6,7 @@
* @version 3.0.0
*/
-use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner as CustomOrdersTableCLIRunner;
+use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner as CustomOrdersTableCLIRunner;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\CLIRunner as ProductAttributesLookupCLIRunner;
defined( 'ABSPATH' ) || exit;
diff --git a/plugins/woocommerce/includes/class-wc-customer-download.php b/plugins/woocommerce/includes/class-wc-customer-download.php
index 43d5a2101af..7d879b7801b 100644
--- a/plugins/woocommerce/includes/class-wc-customer-download.php
+++ b/plugins/woocommerce/includes/class-wc-customer-download.php
@@ -171,13 +171,8 @@ class WC_Customer_Download extends WC_Data implements ArrayAccess {
*/
public function get_download_count( $context = 'view' ) {
// Check for count of download logs.
- $data_store = WC_Data_Store::load( 'customer-download-log' );
- $download_log_ids = $data_store->get_download_logs_for_permission( $this->get_id() );
-
- $download_log_count = 0;
- if ( ! empty( $download_log_ids ) ) {
- $download_log_count = count( $download_log_ids );
- }
+ $data_store = WC_Data_Store::load( 'customer-download-log' );
+ $download_log_count = $data_store->get_download_logs_count_for_permission( $this->get_id() );
// Check download count in prop.
$download_count_prop = $this->get_prop( 'download_count', $context );
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index 24a9000b0a4..6b7a66da18e 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -17,7 +17,7 @@ use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchro
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper as WCConnectionHelper;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
-use Automattic\WooCommerce\Utilities\OrderUtil;
+use Automattic\WooCommerce\Utilities\{ OrderUtil, PluginUtil };
use Automattic\WooCommerce\Internal\Utilities\PluginInstaller;
defined( 'ABSPATH' ) || exit;
@@ -257,6 +257,10 @@ class WC_Install {
),
'9.1.0' => array(
'wc_update_910_add_launch_your_store_tour_option',
+ 'wc_update_910_remove_obsolete_user_meta',
+ ),
+ '9.2.0' => array(
+ 'wc_update_920_add_wc_hooked_blocks_version_option',
),
);
@@ -1263,7 +1267,8 @@ class WC_Install {
return;
}
- if ( in_array( $legacy_api_plugin, wp_get_active_and_valid_plugins(), true ) ) {
+ $active_valid_plugins = wc_get_container()->get( PluginUtil::class )->get_all_active_valid_plugins();
+ if ( in_array( $legacy_api_plugin, $active_valid_plugins, true ) ) {
return;
}
diff --git a/plugins/woocommerce/includes/class-wc-product-simple.php b/plugins/woocommerce/includes/class-wc-product-simple.php
index e49f4959d0f..98bb00f3ffe 100644
--- a/plugins/woocommerce/includes/class-wc-product-simple.php
+++ b/plugins/woocommerce/includes/class-wc-product-simple.php
@@ -74,4 +74,28 @@ class WC_Product_Simple extends WC_Product {
return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( $text, $this->get_name() ), $this );
}
+
+ /**
+ * Get the add to cart button success message - used to update the mini cart live region.
+ *
+ * @return string
+ */
+ public function add_to_cart_success_message() {
+ $text = '';
+
+ if ( $this->is_purchasable() && $this->is_in_stock() ) {
+ /* translators: %s: Product title */
+ $text = __( '“%s” has been added to your cart', 'woocommerce' );
+ $text = sprintf( $text, $this->get_name() );
+ }
+
+ /**
+ * Filter product add to cart success message.
+ *
+ * @since 9.2.0
+ * @param string $text The success message when a product is added to the cart.
+ * @param WC_Product_Simple $this Reference to the current WC_Product_Simple instance.
+ */
+ return apply_filters( 'woocommerce_product_add_to_cart_success_message', $text, $this );
+ }
}
diff --git a/plugins/woocommerce/includes/class-wc-tracker.php b/plugins/woocommerce/includes/class-wc-tracker.php
index f41f30fbab1..5ee937b6e1a 100644
--- a/plugins/woocommerce/includes/class-wc-tracker.php
+++ b/plugins/woocommerce/includes/class-wc-tracker.php
@@ -965,6 +965,7 @@ class WC_Tracker {
'hpos_transactions_enabled' => get_option( 'woocommerce_use_db_transactions_for_custom_orders_table_data_sync' ),
'hpos_transactions_level' => get_option( 'woocommerce_db_transactions_isolation_level_for_custom_orders_table_data_sync' ),
'show_marketplace_suggestions' => get_option( 'woocommerce_show_marketplace_suggestions' ),
+ 'admin_install_timestamp' => get_option( 'woocommerce_admin_install_timestamp' ),
);
}
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index 9eb3f3c003d..3dba08bbeb3 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -26,10 +26,9 @@ use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Internal\Utilities\LegacyRestApiStub;
use Automattic\WooCommerce\Internal\Utilities\WebhookUtil;
use Automattic\WooCommerce\Internal\Admin\Marketplace;
+use Automattic\WooCommerce\Internal\McStats;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\{LoggingUtil, RestApiUtil, TimeUtil};
-use Automattic\WooCommerce\Admin\WCAdminHelper;
-use Automattic\WooCommerce\Admin\Features\Features;
/**
* Main WooCommerce Class.
@@ -45,7 +44,7 @@ final class WooCommerce {
*
* @var string
*/
- public $version = '9.2.0';
+ public $version = '9.3.0';
/**
* WooCommerce Schema version.
@@ -54,7 +53,7 @@ final class WooCommerce {
*
* @var string
*/
- public $db_version = '430';
+ public $db_version = '920';
/**
* The single instance of the class.
@@ -307,7 +306,9 @@ final class WooCommerce {
self::add_action( 'rest_api_init', array( $this, 'register_wp_admin_settings' ) );
add_action( 'woocommerce_installed', array( $this, 'add_woocommerce_remote_variant' ) );
add_action( 'woocommerce_updated', array( $this, 'add_woocommerce_remote_variant' ) );
+ add_action( 'woocommerce_newly_installed', 'wc_set_hooked_blocks_version', 10 );
+ self::add_filter( 'robots_txt', array( $this, 'robots_txt' ) );
add_filter( 'wp_plugin_dependencies_slug', array( $this, 'convert_woocommerce_slug' ) );
// These classes set up hooks on instantiation.
@@ -402,6 +403,12 @@ final class WooCommerce {
$context
);
+ // Record fatal error stats.
+ $container = wc_get_container();
+ $mc_stats = $container->get( McStats::class );
+ $mc_stats->add( 'error', 'fatal-errors-during-shutdown' );
+ $mc_stats->do_server_side_stats();
+
/**
* Action triggered when there are errors during shutdown.
*
@@ -1038,6 +1045,43 @@ final class WooCommerce {
}
}
+ /**
+ * Tell bots not to index some WooCommerce-created directories.
+ *
+ * We try to detect the default "User-agent: *" added by WordPress and add our rules to that group, because
+ * it's possible that some bots will only interpret the first group of rules if there are multiple groups with
+ * the same user agent.
+ *
+ * @param string $output The contents that WordPress will output in a robots.txt file.
+ *
+ * @return string
+ */
+ private function robots_txt( $output ) {
+ $path = ( ! empty( $site_url['path'] ) ) ? $site_url['path'] : '';
+
+ $lines = preg_split( '/\r\n|\r|\n/', $output );
+ $agent_index = array_search( 'User-agent: *', $lines, true );
+
+ if ( false !== $agent_index ) {
+ $above = array_slice( $lines, 0, $agent_index + 1 );
+ $below = array_slice( $lines, $agent_index + 1 );
+ } else {
+ $above = $lines;
+ $below = array();
+
+ $above[] = '';
+ $above[] = 'User-agent: *';
+ }
+
+ $above[] = "Disallow: $path/wp-content/uploads/wc-logs/";
+ $above[] = "Disallow: $path/wp-content/uploads/woocommerce_transient_files/";
+ $above[] = "Disallow: $path/wp-content/uploads/woocommerce_uploads/";
+
+ $lines = array_merge( $above, $below );
+
+ return implode( PHP_EOL, $lines );
+ }
+
/**
* Set tablenames inside WPDB object.
*/
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php
index 5219aff2867..82e129cadcd 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php
@@ -7,6 +7,7 @@
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
+use Automattic\WooCommerce\Internal\Utilities\Users;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -340,17 +341,36 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
* @return WC_Order|false
*/
public function get_last_order( &$customer ) {
- //phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
+ // Try to fetch the last order placed by this customer.
+ $last_order_id = Users::get_site_user_meta( $customer->get_id(), 'wc_last_order', true );
+ $last_customer_order = false;
+
+ if ( ! empty( $last_order_id ) ) {
+ $last_customer_order = wc_get_order( $last_order_id );
+ }
+
+ // "Unset" the last order ID if the order is associated with another customer. Unsetting is done by making it an
+ // empty string, for compatibility with the declared types of the following filter hook.
+ if (
+ ! $last_customer_order instanceof WC_Order
+ || intval( $last_customer_order->get_customer_id() ) !== intval( $customer->get_id() )
+ ) {
+ $last_order_id = '';
+ }
+
/**
* Filters the id of the last order from a given customer.
*
- * @param string @last_order_id The last order id as retrieved from the database.
- * @param WC_Customer The customer whose last order id is being retrieved.
+ * @since 4.9.1
+ *
+ * @param string $last_order_id The last order id as retrieved from the database.
+ * @param WC_Customer $customer The customer whose last order id is being retrieved.
+ *
* @return string The actual last order id to use.
*/
$last_order_id = apply_filters(
'woocommerce_customer_get_last_order',
- get_user_meta( $customer->get_id(), '_last_order', true ),
+ $last_order_id,
$customer
);
//phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
@@ -385,7 +405,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
);
}
//phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
- update_user_meta( $customer->get_id(), '_last_order', $last_order_id );
+ Users::update_site_user_meta( $customer->get_id(), 'wc_last_order', $last_order_id );
}
if ( ! $last_order_id ) {
@@ -405,7 +425,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
public function get_order_count( &$customer ) {
$count = apply_filters(
'woocommerce_customer_get_order_count',
- get_user_meta( $customer->get_id(), '_order_count', true ),
+ Users::get_site_user_meta( $customer->get_id(), 'wc_order_count', true ),
$customer
);
@@ -436,7 +456,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
}
//phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
- update_user_meta( $customer->get_id(), '_order_count', $count );
+ Users::update_site_user_meta( $customer->get_id(), 'wc_order_count', $count );
}
return absint( $count );
@@ -452,7 +472,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
public function get_total_spent( &$customer ) {
$spent = apply_filters(
'woocommerce_customer_get_total_spent',
- get_user_meta( $customer->get_id(), '_money_spent', true ),
+ Users::get_site_user_meta( $customer->get_id(), 'wc_money_spent', true ),
$customer
);
@@ -499,7 +519,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
if ( ! $spent ) {
$spent = 0;
}
- update_user_meta( $customer->get_id(), '_money_spent', $spent );
+ Users::update_site_user_meta( $customer->get_id(), 'wc_money_spent', $spent );
}
return wc_format_decimal( $spent, 2 );
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-customer-download-log-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-customer-download-log-data-store.php
index 1001e7001ab..2f56df7666e 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-customer-download-log-data-store.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-customer-download-log-data-store.php
@@ -149,10 +149,10 @@ class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Da
}
/**
- * Get array of download log ids by specified args.
+ * Get array of download logs, or the count of existing logs, by specified args.
*
- * @param array $args Arguments to define download logs to retrieve.
- * @return array
+ * @param array $args Arguments to define download logs to retrieve. If $args['return'] is 'count' then the count of existing logs will be returned.
+ * @return array|int
*/
public function get_download_logs( $args = array() ) {
global $wpdb;
@@ -171,9 +171,11 @@ class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Da
)
);
+ $is_count = 'count' === $args['return'];
+
$query = array();
$table = $wpdb->prefix . self::get_table_name();
- $query[] = "SELECT * FROM {$table} WHERE 1=1";
+ $query[] = 'SELECT ' . ( $is_count ? 'COUNT(1)' : '*' ) . " FROM {$table} WHERE 1=1";
if ( $args['permission_id'] ) {
$query[] = $wpdb->prepare( 'AND permission_id = %d', $args['permission_id'] );
@@ -197,7 +199,13 @@ class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Da
$query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) );
}
- $raw_download_logs = $wpdb->get_results( implode( ' ', $query ) ); // WPCS: unprepared SQL ok.
+ // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
+ if ( $is_count ) {
+ return absint( $wpdb->get_var( implode( ' ', $query ) ) );
+ }
+
+ $raw_download_logs = $wpdb->get_results( implode( ' ', $query ) );
+ // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
switch ( $args['return'] ) {
case 'ids':
@@ -226,6 +234,26 @@ class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Da
);
}
+ /**
+ * Get the count of download logs for a given download permission.
+ *
+ * @param int $permission_id Permission to get logs count for.
+ * @return int
+ */
+ public function get_download_logs_count_for_permission( $permission_id ) {
+ // If no permission_id is passed, return an empty array.
+ if ( empty( $permission_id ) ) {
+ return 0;
+ }
+
+ return $this->get_download_logs(
+ array(
+ 'permission_id' => $permission_id,
+ 'return' => 'count',
+ )
+ );
+ }
+
/**
* Method to delete download logs for a given permission ID.
*
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php
index 7882f1673aa..9a96390266e 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php
@@ -5,6 +5,8 @@
* @package WooCommerce\Classes
*/
+use Automattic\WooCommerce\Utilities\OrderUtil;
+
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -189,22 +191,37 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
// Also grab the current status so we can compare.
$previous_status = get_post_status( $order->get_id() );
+ // If the order doesn't exist in the DB, we will consider it as new.
+ if ( ! $previous_status && $order->get_id() === 0 ) {
+ $previous_status = 'new';
+ }
// Update the order.
parent::update( $order );
- // Fire a hook depending on the status - this should be considered a creation if it was previously draft status.
- $new_status = $order->get_status( 'edit' );
+ $current_status = $order->get_status( 'edit' );
- if ( $new_status !== $previous_status && in_array( $previous_status, array( 'new', 'auto-draft', 'draft', 'checkout-draft' ), true ) ) {
- do_action( 'woocommerce_new_order', $order->get_id(), $order );
- } else {
- do_action( 'woocommerce_update_order', $order->get_id(), $order );
+ // We need to remove the wc- prefix from the status for comparison and proper evaluation of new vs updated orders.
+ $previous_status = OrderUtil::remove_status_prefix( $previous_status );
+ $current_status = OrderUtil::remove_status_prefix( $current_status );
+
+ $draft_statuses = array( 'new', 'auto-draft', 'draft', 'checkout-draft' );
+
+ // This hook should be fired only if the new status is not one of draft statuses and the previous status was one of the draft statuses.
+ if (
+ $current_status !== $previous_status
+ && ! in_array( $current_status, $draft_statuses, true )
+ && in_array( $previous_status, $draft_statuses, true )
+ ) {
+ do_action( 'woocommerce_new_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+ return;
}
+
+ do_action( 'woocommerce_update_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
}
/**
- * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class.
+ * Helper method that updates all the post meta for an order based on its settings in the WC_Order class.
*
* @param WC_Order $order Order object.
* @since 3.0.0
@@ -1003,6 +1020,40 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
* @return array|object
*/
public function query( $query_vars ) {
+ /**
+ * Allows 3rd parties to filter query args that will trigger an unsupported notice.
+ *
+ * @since 9.2.0
+ *
+ * @param array $unsupported_args Array of query arg names.
+ */
+ $unsupported_args = (array) apply_filters(
+ 'woocommerce_order_data_store_cpt_query_unsupported_args',
+ array( 'meta_query', 'field_query' )
+ );
+
+ // Trigger doing_it_wrong() for query vars only supported in HPOS.
+ $unsupported_args_in_query = array_keys( array_filter( array_intersect_key( $query_vars, array_flip( $unsupported_args ) ) ) );
+
+ if ( $unsupported_args_in_query && __CLASS__ === get_class( $this ) ) {
+ wc_doing_it_wrong(
+ __METHOD__,
+ esc_html(
+ sprintf(
+ // translators: %s is a comma separated list of query arguments.
+ _n(
+ 'Order query argument (%s) is not supported on the current order datastore.',
+ 'Order query arguments (%s) are not supported on the current order datastore.',
+ count( $unsupported_args_in_query ),
+ 'woocommerce'
+ ),
+ implode( ', ', $unsupported_args_in_query )
+ )
+ ),
+ '9.2.0'
+ );
+ }
+
$args = $this->get_wp_query_args( $query_vars );
if ( ! empty( $args['errors'] ) ) {
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
index 7bdc8bd3368..5087a4beb30 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
@@ -334,7 +334,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
$product->apply_changes();
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
- do_action( 'woocommerce_update_product', $product->get_id(), $product, $changes );
+ do_action( 'woocommerce_update_product', $product->get_id(), $product );
}
/**
@@ -2237,23 +2237,26 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
$stock = 'yes' === $manage_stock ? wc_stock_amount( get_post_meta( $id, '_stock', true ) ) : null;
$price = wc_format_decimal( get_post_meta( $id, '_price', true ) );
$sale_price = wc_format_decimal( get_post_meta( $id, '_sale_price', true ) );
- return array(
- 'product_id' => absint( $id ),
- 'sku' => get_post_meta( $id, '_sku', true ),
- 'global_unique_id' => get_post_meta( $id, '_global_unique_id', true ),
- 'virtual' => 'yes' === get_post_meta( $id, '_virtual', true ) ? 1 : 0,
- 'downloadable' => 'yes' === get_post_meta( $id, '_downloadable', true ) ? 1 : 0,
- 'min_price' => reset( $price_meta ),
- 'max_price' => end( $price_meta ),
- 'onsale' => $sale_price && $price === $sale_price ? 1 : 0,
- 'stock_quantity' => $stock,
- 'stock_status' => get_post_meta( $id, '_stock_status', true ),
- 'rating_count' => array_sum( array_map( 'intval', (array) get_post_meta( $id, '_wc_rating_count', true ) ) ),
- 'average_rating' => get_post_meta( $id, '_wc_average_rating', true ),
- 'total_sales' => get_post_meta( $id, 'total_sales', true ),
- 'tax_status' => get_post_meta( $id, '_tax_status', true ),
- 'tax_class' => get_post_meta( $id, '_tax_class', true ),
+ $product_data = array(
+ 'product_id' => absint( $id ),
+ 'sku' => get_post_meta( $id, '_sku', true ),
+ 'virtual' => 'yes' === get_post_meta( $id, '_virtual', true ) ? 1 : 0,
+ 'downloadable' => 'yes' === get_post_meta( $id, '_downloadable', true ) ? 1 : 0,
+ 'min_price' => reset( $price_meta ),
+ 'max_price' => end( $price_meta ),
+ 'onsale' => $sale_price && $price === $sale_price ? 1 : 0,
+ 'stock_quantity' => $stock,
+ 'stock_status' => get_post_meta( $id, '_stock_status', true ),
+ 'rating_count' => array_sum( array_map( 'intval', (array) get_post_meta( $id, '_wc_rating_count', true ) ) ),
+ 'average_rating' => get_post_meta( $id, '_wc_average_rating', true ),
+ 'total_sales' => get_post_meta( $id, 'total_sales', true ),
+ 'tax_status' => get_post_meta( $id, '_tax_status', true ),
+ 'tax_class' => get_post_meta( $id, '_tax_class', true ),
);
+ if ( get_option( 'woocommerce_schema_version', 0 ) >= 920 ) {
+ $product_data['global_unique_id'] = get_post_meta( $id, '_global_unique_id', true );
+ }
+ return $product_data;
}
return array();
}
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php
index 210efaa79b7..72bacd6bf30 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php
@@ -144,6 +144,11 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
+ $review = $this->get_review( (int) $request['id'], (int) $request['product_id'] );
+ if ( is_wp_error( $review ) ) {
+ return $review;
+ }
+
if ( ! wc_rest_check_product_reviews_permissions( 'read', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -172,6 +177,11 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
+ $review = $this->get_review( (int) $request['id'], (int) $request['product_id'] );
+ if ( is_wp_error( $review ) ) {
+ return $review;
+ }
+
if ( ! wc_rest_check_product_reviews_permissions( 'edit', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -186,6 +196,11 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
* @return WP_Error|boolean
*/
public function delete_item_permissions_check( $request ) {
+ $review = $this->get_review( (int) $request['id'], (int) $request['product_id'] );
+ if ( is_wp_error( $review ) ) {
+ return $review;
+ }
+
if ( ! wc_rest_check_product_reviews_permissions( 'delete', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -218,6 +233,28 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
return rest_ensure_response( $data );
}
+ /**
+ * Fetch a single product review from the database.
+ *
+ * @param int $id Review ID.
+ * @param int $product_id Product ID.
+ *
+ * @since 9.2.0
+ * @return \WP_Comment
+ */
+ protected function get_review( int $id, int $product_id ) {
+ if ( 0 >= $product_id || 'product' !== get_post_type( $product_id ) ) {
+ return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
+ }
+
+ $review = 0 <= $id ? get_comment( $id ) : null;
+ if ( empty( $review ) || empty( $review->comment_ID ) || 'review' !== get_comment_type( $id ) || empty( $review->comment_post_ID ) || (int) $review->comment_post_ID !== $product_id ) {
+ return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) );
+ }
+
+ return $review;
+ }
+
/**
* Get a single product review.
*
@@ -225,17 +262,9 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
- $id = (int) $request['id'];
- $product_id = (int) $request['product_id'];
-
- if ( 'product' !== get_post_type( $product_id ) ) {
- return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
- }
-
- $review = get_comment( $id );
-
- if ( empty( $id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) {
- return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
+ $review = $this->get_review( (int) $request['id'], (int) $request['product_id'] );
+ if ( is_wp_error( $review ) ) {
+ return $review;
}
$delivery = $this->prepare_item_for_response( $review, $request );
@@ -309,14 +338,9 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
$product_review_id = (int) $request['id'];
$product_id = (int) $request['product_id'];
- if ( 'product' !== get_post_type( $product_id ) ) {
- return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
- }
-
- $review = get_comment( $product_review_id );
-
- if ( empty( $product_review_id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) {
- return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
+ $review = $this->get_review( $product_review_id, $product_id );
+ if ( is_wp_error( $review ) ) {
+ return $review;
}
$prepared_review = $this->prepare_item_for_database( $request );
@@ -358,15 +382,11 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
public function delete_item( $request ) {
$product_id = (int) $request['product_id'];
$product_review_id = (int) $request['id'];
- $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
+ $product_review = $this->get_review( $product_review_id, $product_id );
+ $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
- if ( 'product' !== get_post_type( $product_id ) ) {
- return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
- }
-
- $product_review = get_comment( $product_review_id );
- if ( empty( $product_review_id ) || empty( $product_review->comment_ID ) || empty( $product_review->comment_post_ID ) ) {
- return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) );
+ if ( is_wp_error( $product_review ) ) {
+ return $product_review;
}
/**
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
index a8aa9c46ede..aeebf511d81 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
@@ -468,7 +468,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
}
// Format the order status.
- $data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];
+ $data['status'] = OrderUtil::remove_status_prefix( $data['status'] );
// Format line items.
foreach ( $format_line_items as $key ) {
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php
index e9da6f31cdd..873d7863c7f 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php
@@ -13,7 +13,7 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer as Order_DataSynchronizer;
-use Automattic\WooCommerce\Utilities\{ LoggingUtil, OrderUtil };
+use Automattic\WooCommerce\Utilities\{ LoggingUtil, OrderUtil, PluginUtil };
/**
* System status controller class.
@@ -1044,16 +1044,11 @@ class WC_REST_System_Status_V2_Controller extends WC_REST_Controller {
return array();
}
- $active_plugins = (array) get_option( 'active_plugins', array() );
- if ( is_multisite() ) {
- $network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
- $active_plugins = array_merge( $active_plugins, $network_activated_plugins );
- }
+ $active_valid_plugins = wc_get_container()->get( PluginUtil::class )->get_all_active_valid_plugins();
+ $active_plugins_data = array();
- $active_plugins_data = array();
-
- foreach ( $active_plugins as $plugin ) {
- $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
+ foreach ( $active_valid_plugins as $plugin ) {
+ $data = get_plugin_data( $plugin );
$active_plugins_data[] = $this->format_plugin_data( $plugin, $data );
}
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php
index b2081f9cce3..46fad08b36a 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php
@@ -149,6 +149,11 @@ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller {
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
+ $review = $this->get_review( (int) $request['id'] );
+ if ( is_wp_error( $review ) ) {
+ return $review;
+ }
+
if ( ! wc_rest_check_product_reviews_permissions( 'read', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -177,6 +182,11 @@ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller {
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
+ $review = $this->get_review( (int) $request['id'] );
+ if ( is_wp_error( $review ) ) {
+ return $review;
+ }
+
if ( ! wc_rest_check_product_reviews_permissions( 'edit', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -191,6 +201,11 @@ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller {
* @return WP_Error|boolean
*/
public function delete_item_permissions_check( $request ) {
+ $review = $this->get_review( (int) $request['id'] );
+ if ( is_wp_error( $review ) ) {
+ return $review;
+ }
+
if ( ! wc_rest_check_product_reviews_permissions( 'delete', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -1057,13 +1072,11 @@ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller {
}
$review = get_comment( $id );
- if ( empty( $review ) ) {
+ if ( empty( $review ) || 'review' !== get_comment_type( $id ) ) {
return $error;
}
if ( ! empty( $review->comment_post_ID ) ) {
- $post = get_post( (int) $review->comment_post_ID );
-
if ( 'product' !== get_post_type( (int) $review->comment_post_ID ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
diff --git a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
index 49dafb20c7e..5873d403e45 100644
--- a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
+++ b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
@@ -39,7 +39,12 @@ class WC_Shortcode_My_Account {
return;
}
- if ( ! is_user_logged_in() || isset( $wp->query_vars['lost-password'] ) ) {
+ if ( ! is_user_logged_in() ) {
+ /**
+ * Filters the message shown on the 'my account' page when the user is not logged in.
+ *
+ * @since 2.6.0
+ */
$message = apply_filters( 'woocommerce_my_account_message', '' );
if ( ! empty( $message ) ) {
@@ -56,50 +61,16 @@ class WC_Shortcode_My_Account {
} else {
wc_get_template( 'myaccount/form-login.php' );
}
- } else {
- // Start output buffer since the html may need discarding for BW compatibility.
- ob_start();
-
- if ( isset( $wp->query_vars['customer-logout'] ) ) {
- /* translators: %s: logout url */
- wc_add_notice( sprintf( __( 'Are you sure you want to log out? Confirm and log out ', 'woocommerce' ), wc_logout_url() ) );
- }
-
- // Collect notices before output.
- $notices = wc_get_notices();
-
- // Output the new account page.
- self::my_account( $atts );
-
- /**
- * Deprecated my-account.php template handling. This code should be
- * removed in a future release.
- *
- * If woocommerce_account_content did not run, this is an old template
- * so we need to render the endpoint content again.
- */
- if ( ! did_action( 'woocommerce_account_content' ) ) {
- if ( ! empty( $wp->query_vars ) ) {
- foreach ( $wp->query_vars as $key => $value ) {
- if ( 'pagename' === $key ) {
- continue;
- }
- if ( has_action( 'woocommerce_account_' . $key . '_endpoint' ) ) {
- ob_clean(); // Clear previous buffer.
- wc_set_notices( $notices );
- wc_print_notices();
- do_action( 'woocommerce_account_' . $key . '_endpoint', $value );
- break;
- }
- }
-
- wc_deprecated_function( 'Your theme version of my-account.php template', '2.6', 'the latest version, which supports multiple account pages and navigation, from WC 2.6.0' );
- }
- }
-
- // Send output buffer.
- ob_end_flush();
+ return;
}
+
+ if ( isset( $wp->query_vars['customer-logout'] ) ) {
+ /* translators: %s: logout url */
+ wc_add_notice( sprintf( __( 'Are you sure you want to log out? Confirm and log out ', 'woocommerce' ), wc_logout_url() ) );
+ }
+
+ // Output the my account page.
+ self::my_account( $atts );
}
/**
diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php
index 0021c9afc50..092be4f81ee 100644
--- a/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php
+++ b/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php
@@ -121,7 +121,7 @@ class WC_Product_Collection_Block_Tracking {
'in_single_product' => $is_in_single_product ? 'yes' : 'no',
'in_template_part' => $is_in_template_part ? 'yes' : 'no',
'in_synced_pattern' => $is_in_synced_pattern ? 'yes' : 'no',
- 'filters' => $this->get_query_filters_usage_data( $block ),
+ 'filters' => wp_json_encode( $this->get_query_filters_usage_data( $block ) ),
);
}
diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php
index 3056f5b07d4..91532cf2a9f 100644
--- a/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php
+++ b/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php
@@ -29,8 +29,7 @@ class WC_Products_Tracking {
add_action( 'load-edit.php', array( $this, 'track_products_view' ), 10 );
add_action( 'load-edit-tags.php', array( $this, 'track_categories_and_tags_view' ), 10, 2 );
add_action( 'edit_post', array( $this, 'track_product_updated' ), 10, 2 );
- add_action( 'woocommerce_new_product', array( $this, 'track_product_published' ), 10, 3 );
- add_action( 'woocommerce_update_product', array( $this, 'track_product_published' ), 10, 3 );
+ add_action( 'wp_after_insert_post', array( $this, 'track_product_published' ), 10, 4 );
add_action( 'created_product_cat', array( $this, 'track_product_category_created' ) );
add_action( 'edited_product_cat', array( $this, 'track_product_category_updated' ) );
add_action( 'add_meta_boxes_product', array( $this, 'track_product_updated_client_side' ), 10 );
@@ -304,21 +303,24 @@ class WC_Products_Tracking {
/**
* Send a Tracks event when a product is published.
*
- * @param int $product_id Product ID.
- * @param WC_Product $product Product object.
- * @param array $changes Product changes.
+ * @param int $post_id Post ID.
+ * @param WP_Post $post Post object.
+ * @param bool $update Whether this is an existing post being updated.
+ * @param null|WP_Post $post_before Null for new posts, the WP_Post object prior
+ * to the update for updated posts.
*/
- public function track_product_published( $product_id, $product, $changes = null ) {
+ public function track_product_published( $post_id, $post, $update, $post_before ) {
if (
- ! isset( $product ) ||
- 'product' !== $product->post_type ||
- 'publish' !== $product->get_status( 'edit' ) ||
- ( $changes && ! isset( $changes['status'] ) )
+ 'product' !== $post->post_type ||
+ 'publish' !== $post->post_status ||
+ ( $post_before && 'publish' === $post_before->post_status )
) {
return;
}
- $product_type_options = self::get_product_type_options( $product_id );
+ $product = wc_get_product( $post_id );
+
+ $product_type_options = self::get_product_type_options( $post_id );
$product_type_options_string = self::get_product_type_options_string( $product_type_options );
$properties = array(
@@ -332,7 +334,7 @@ class WC_Products_Tracking {
'is_virtual' => $product->is_virtual() ? 'yes' : 'no',
'manage_stock' => $product->get_manage_stock() ? 'yes' : 'no',
'menu_order' => $product->get_menu_order() ? 'yes' : 'no',
- 'product_id' => $product_id,
+ 'product_id' => $post_id,
'product_gallery' => count( $product->get_gallery_image_ids() ),
'product_image' => $product->get_image_id() ? 'yes' : 'no',
'product_type' => $product->get_type(),
diff --git a/plugins/woocommerce/includes/wc-account-functions.php b/plugins/woocommerce/includes/wc-account-functions.php
index 7b76c515dae..3f34a3a5801 100644
--- a/plugins/woocommerce/includes/wc-account-functions.php
+++ b/plugins/woocommerce/includes/wc-account-functions.php
@@ -176,11 +176,13 @@ function wc_get_account_endpoint_url( $endpoint ) {
return wc_get_page_permalink( 'myaccount' );
}
+ $url = wc_get_endpoint_url( $endpoint, '', wc_get_page_permalink( 'myaccount' ) );
+
if ( 'customer-logout' === $endpoint ) {
- return wc_logout_url();
+ return wp_nonce_url( $url, 'customer-logout' );
}
- return wc_get_endpoint_url( $endpoint, '', wc_get_page_permalink( 'myaccount' ) );
+ return $url;
}
/**
diff --git a/plugins/woocommerce/includes/wc-cart-functions.php b/plugins/woocommerce/includes/wc-cart-functions.php
index c4657188505..b2fb32db9a7 100644
--- a/plugins/woocommerce/includes/wc-cart-functions.php
+++ b/plugins/woocommerce/includes/wc-cart-functions.php
@@ -184,7 +184,7 @@ function wc_clear_cart_after_payment() {
}
}
- if ( WC()->session->order_awaiting_payment > 0 ) {
+ if ( is_object( WC()->session ) && WC()->session->order_awaiting_payment > 0 ) {
$order = wc_get_order( WC()->session->order_awaiting_payment );
if ( $order instanceof WC_Order && $order->get_id() > 0 ) {
diff --git a/plugins/woocommerce/includes/wc-order-functions.php b/plugins/woocommerce/includes/wc-order-functions.php
index c1e4b6d3944..420f2da080d 100644
--- a/plugins/woocommerce/includes/wc-order-functions.php
+++ b/plugins/woocommerce/includes/wc-order-functions.php
@@ -9,6 +9,8 @@
*/
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
+use Automattic\WooCommerce\Internal\Utilities\Users;
+use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Utilities\StringUtil;
defined( 'ABSPATH' ) || exit;
@@ -146,9 +148,9 @@ function wc_get_is_pending_statuses() {
*/
function wc_get_order_status_name( $status ) {
$statuses = wc_get_order_statuses();
- $status = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status;
- $status = isset( $statuses[ 'wc-' . $status ] ) ? $statuses[ 'wc-' . $status ] : $status;
- return $status;
+ $status = OrderUtil::remove_status_prefix( $status );
+
+ return $statuses[ 'wc-' . $status ] ?? $status;
}
/**
@@ -484,9 +486,9 @@ function wc_delete_shop_order_transients( $order = 0 ) {
// Clear customer's order related caches.
if ( is_a( $order, 'WC_Order' ) ) {
$order_id = $order->get_id();
- delete_user_meta( $order->get_customer_id(), '_money_spent' );
- delete_user_meta( $order->get_customer_id(), '_order_count' );
- delete_user_meta( $order->get_customer_id(), '_last_order' );
+ Users::delete_site_user_meta( $order->get_customer_id(), 'wc_money_spent' );
+ Users::delete_site_user_meta( $order->get_customer_id(), 'wc_order_count' );
+ Users::delete_site_user_meta( $order->get_customer_id(), 'wc_last_order' );
} else {
$order_id = 0;
}
diff --git a/plugins/woocommerce/includes/wc-page-functions.php b/plugins/woocommerce/includes/wc-page-functions.php
index 2b0a03f05e8..f80ac3e54a0 100644
--- a/plugins/woocommerce/includes/wc-page-functions.php
+++ b/plugins/woocommerce/includes/wc-page-functions.php
@@ -119,26 +119,32 @@ function wc_get_endpoint_url( $endpoint, $value = '', $permalink = '' ) {
}
/**
- * Hide menu items conditionally.
+ * Hide or adjust menu items conditionally.
*
* @param array $items Navigation items.
* @return array
*/
function wc_nav_menu_items( $items ) {
- if ( ! is_user_logged_in() ) {
- $customer_logout = get_option( 'woocommerce_logout_endpoint', 'customer-logout' );
+ $logout_endpoint = get_option( 'woocommerce_logout_endpoint', 'customer-logout' );
- if ( ! empty( $customer_logout ) && ! empty( $items ) && is_array( $items ) ) {
- foreach ( $items as $key => $item ) {
- if ( empty( $item->url ) ) {
- continue;
- }
- $path = wp_parse_url( $item->url, PHP_URL_PATH ) ?? '';
- $query = wp_parse_url( $item->url, PHP_URL_QUERY ) ?? '';
+ if ( ! empty( $logout_endpoint ) && ! empty( $items ) && is_array( $items ) ) {
+ foreach ( $items as $key => $item ) {
+ if ( empty( $item->url ) ) {
+ continue;
+ }
- if ( strstr( $path, $customer_logout ) || strstr( $query, $customer_logout ) ) {
- unset( $items[ $key ] );
- }
+ $path = wp_parse_url( $item->url, PHP_URL_PATH ) ?? '';
+ $query = wp_parse_url( $item->url, PHP_URL_QUERY ) ?? '';
+ $is_logout_link = strstr( $path, $logout_endpoint ) || strstr( $query, $logout_endpoint );
+
+ if ( ! $is_logout_link ) {
+ continue;
+ }
+
+ if ( is_user_logged_in() ) {
+ $items[ $key ]->url = wp_nonce_url( $item->url, 'customer-logout' );
+ } else {
+ unset( $items[ $key ] );
}
}
}
@@ -147,6 +153,40 @@ function wc_nav_menu_items( $items ) {
}
add_filter( 'wp_nav_menu_objects', 'wc_nav_menu_items', 10 );
+/**
+ * Hide menu items in navigation blocks conditionally.
+ *
+ * Does the same thing as wc_nav_menu_items but for block themes.
+ *
+ * @since 9.3.0
+ * @param \WP_Block_list $inner_blocks Inner blocks.
+ * @return \WP_Block_list
+ */
+function wc_nav_menu_inner_blocks( $inner_blocks ) {
+ $logout_endpoint = get_option( 'woocommerce_logout_endpoint', 'customer-logout' );
+
+ if ( ! empty( $logout_endpoint ) && $inner_blocks ) {
+ foreach ( $inner_blocks as $inner_block_key => $inner_block ) {
+ $url = $inner_block->parsed_block['attrs']['url'] ?? '';
+ $path = wp_parse_url( $url, PHP_URL_PATH ) ?? '';
+ $query = wp_parse_url( $url, PHP_URL_QUERY ) ?? '';
+ $is_logout_link = strstr( $path, $logout_endpoint ) || strstr( $query, $logout_endpoint );
+
+ if ( ! $is_logout_link ) {
+ continue;
+ }
+
+ if ( is_user_logged_in() ) {
+ $inner_block->parsed_block['attrs']['url'] = wp_nonce_url( $inner_block->parsed_block['attrs']['url'], 'customer-logout' );
+ } else {
+ unset( $inner_blocks[ $inner_block_key ] );
+ }
+ }
+ }
+
+ return $inner_blocks;
+}
+add_filter( 'block_core_navigation_render_inner_blocks', 'wc_nav_menu_inner_blocks' );
/**
* Fix active class in nav for shop page.
@@ -168,7 +208,7 @@ function wc_nav_menu_item_classes( $menu_items ) {
$menu_id = (int) $menu_item->object_id;
// Unset active class for blog page.
- if ( $page_for_posts === $menu_id ) {
+ if ( $page_for_posts === $menu_id && isset( $menu_item->object ) && 'page' === $menu_item->object ) {
$menu_items[ $key ]->current = false;
if ( in_array( 'current_page_parent', $classes, true ) ) {
diff --git a/plugins/woocommerce/includes/wc-product-functions.php b/plugins/woocommerce/includes/wc-product-functions.php
index 92341e606ee..9e49568115e 100644
--- a/plugins/woocommerce/includes/wc-product-functions.php
+++ b/plugins/woocommerce/includes/wc-product-functions.php
@@ -1571,6 +1571,7 @@ function wc_update_product_lookup_tables_column( $column ) {
);
break;
case 'sku':
+ case 'global_unique_id':
case 'stock_status':
case 'average_rating':
case 'total_sales':
diff --git a/plugins/woocommerce/includes/wc-stock-functions.php b/plugins/woocommerce/includes/wc-stock-functions.php
index 4aee1011828..9ad42486d7b 100644
--- a/plugins/woocommerce/includes/wc-stock-functions.php
+++ b/plugins/woocommerce/includes/wc-stock-functions.php
@@ -10,6 +10,8 @@
defined( 'ABSPATH' ) || exit;
+use Automattic\WooCommerce\Checkout\Helpers\ReserveStock;
+
/**
* Update a product's stock amount.
*
@@ -348,7 +350,8 @@ function wc_get_held_stock_quantity( WC_Product $product, $exclude_order_id = 0
return 0;
}
- return ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->get_reserved_stock( $product, $exclude_order_id );
+ $reserve_stock = new ReserveStock();
+ return $reserve_stock->get_reserved_stock( $product, $exclude_order_id );
}
/**
@@ -374,7 +377,8 @@ function wc_reserve_stock_for_order( $order ) {
$order = $order instanceof WC_Order ? $order : wc_get_order( $order );
if ( $order ) {
- ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->reserve_stock_for_order( $order );
+ $reserve_stock = new ReserveStock();
+ $reserve_stock->reserve_stock_for_order( $order );
}
}
add_action( 'woocommerce_checkout_order_created', 'wc_reserve_stock_for_order' );
@@ -400,7 +404,8 @@ function wc_release_stock_for_order( $order ) {
$order = $order instanceof WC_Order ? $order : wc_get_order( $order );
if ( $order ) {
- ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->release_stock_for_order( $order );
+ $reserve_stock = new ReserveStock();
+ $reserve_stock->release_stock_for_order( $order );
}
}
add_action( 'woocommerce_checkout_order_exception', 'wc_release_stock_for_order' );
diff --git a/plugins/woocommerce/includes/wc-template-functions.php b/plugins/woocommerce/includes/wc-template-functions.php
index 6bd3be8ce29..61842621efb 100644
--- a/plugins/woocommerce/includes/wc-template-functions.php
+++ b/plugins/woocommerce/includes/wc-template-functions.php
@@ -31,18 +31,24 @@ function wc_template_redirect() {
if ( is_page( wc_get_page_id( 'checkout' ) ) && wc_get_page_id( 'checkout' ) !== wc_get_page_id( 'cart' ) && WC()->cart->is_empty() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_redirect_empty_cart', true ) ) {
wp_safe_redirect( wc_get_cart_url() );
exit;
-
}
- // Logout.
- if ( isset( $wp->query_vars['customer-logout'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) {
- wp_safe_redirect( str_replace( '&', '&', wp_logout_url( apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) ) ) ) );
+ // Logout endpoint under My Account page. Logging out requires a valid nonce.
+ if ( isset( $wp->query_vars['customer-logout'] ) ) {
+ if ( ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) {
+ wp_logout();
+ wp_safe_redirect( wc_get_logout_redirect_url() );
+ exit;
+ }
+ /* translators: %s: logout url */
+ wc_add_notice( sprintf( __( 'Are you sure you want to log out? Confirm and log out ', 'woocommerce' ), wc_logout_url() ) );
+ wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) );
exit;
}
- // Redirect to the correct logout endpoint.
- if ( isset( $wp->query_vars['customer-logout'] ) && 'true' === $wp->query_vars['customer-logout'] ) {
- wp_safe_redirect( esc_url_raw( wc_get_account_endpoint_url( 'customer-logout' ) ) );
+ // Redirect to edit account if trying to recover password whilst logged in.
+ if ( isset( $wp->query_vars['lost-password'] ) && is_user_logged_in() ) {
+ wp_safe_redirect( esc_url_raw( wc_get_endpoint_url( 'edit-account', '', wc_get_page_permalink( 'myaccount' ) ) ) );
exit;
}
@@ -1387,6 +1393,10 @@ if ( ! function_exists( 'woocommerce_template_loop_add_to_cart' ) ) {
),
);
+ if ( is_a( $product, 'WC_Product_Simple' ) ) {
+ $defaults['attributes']['data-success_message'] = $product->add_to_cart_success_message();
+ }
+
$args = apply_filters( 'woocommerce_loop_add_to_cart_args', wp_parse_args( $args, $defaults ), $product );
if ( ! empty( $args['attributes']['aria-describedby'] ) ) {
@@ -2854,6 +2864,12 @@ if ( ! function_exists( 'woocommerce_form_field' ) ) {
}
if ( $args['required'] ) {
+ // hidden inputs are the only kind of inputs that don't need an `aria-required` attribute.
+ // checkboxes apply the `custom_attributes` to the label - we need to apply the attribute on the input itself, instead.
+ if ( ! in_array( $args['type'], array( 'hidden', 'checkbox' ), true ) ) {
+ $args['custom_attributes']['aria-required'] = 'true';
+ }
+
$args['class'][] = 'validate-required';
$required = ' * ';
} else {
@@ -2978,12 +2994,13 @@ if ( ! function_exists( 'woocommerce_form_field' ) ) {
}
$field .= sprintf(
- ' %6$s',
+ ' %7$s',
esc_attr( $key ),
esc_attr( $args['id'] ),
esc_attr( $args['checked_value'] ),
esc_attr( 'input-checkbox ' . implode( ' ', $args['input_class'] ) ),
checked( $value, $args['checked_value'], false ),
+ $args['required'] ? ' aria-required="true"' : '',
wp_kses_post( $args['label'] )
);
@@ -3728,22 +3745,31 @@ function wc_get_price_html_from_text() {
}
/**
- * Get logout endpoint.
+ * Get the redirect URL after logging out. Defaults to the my account page.
+ *
+ * @since 9.3.0
+ * @return string
+ */
+function wc_get_logout_redirect_url() {
+ /**
+ * Filters the logout redirect URL.
+ *
+ * @since 2.6.9
+ * @param string $logout_url Logout URL.
+ * @return string
+ */
+ return apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) );
+}
+
+/**
+ * Get logout link.
*
* @since 2.6.9
- *
* @param string $redirect Redirect URL.
- *
* @return string
*/
function wc_logout_url( $redirect = '' ) {
- $redirect = $redirect ? $redirect : apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) );
-
- if ( get_option( 'woocommerce_logout_endpoint' ) ) {
- return wp_nonce_url( wc_get_endpoint_url( 'customer-logout', '', $redirect ), 'customer-logout' );
- }
-
- return wp_logout_url( $redirect );
+ return wp_logout_url( $redirect ? $redirect : wc_get_logout_redirect_url() );
}
/**
@@ -4019,3 +4045,45 @@ function wc_update_product_archive_title( $post_type_name, $post_type ) {
add_filter( 'post_type_archive_title', 'wc_update_product_archive_title', 10, 2 );
// phpcs:enable Generic.Commenting.Todo.TaskFound
+
+/**
+ * Set the version of the hooked blocks in the database. Used when WC is installed for the first time.
+ *
+ * @since 9.2.0
+ *
+ * @return void
+ */
+function wc_set_hooked_blocks_version() {
+ // Only set the version if the current theme is a block theme.
+ if ( ! wc_current_theme_is_fse_theme() && ! current_theme_supports( 'block-template-parts' ) ) {
+ return;
+ }
+
+ $option_name = 'woocommerce_hooked_blocks_version';
+
+ if ( get_option( $option_name ) ) {
+ return;
+ }
+
+ add_option( $option_name, WC()->version );
+}
+
+/**
+ * If the user switches from a classic to a block theme and they haven't already got a woocommerce_hooked_blocks_version,
+ * set the version of the hooked blocks in the database, or as "no" to disable all block hooks then set as the latest WC version.
+ *
+ * @since 9.2.0
+ *
+ * @param string $old_name Old theme name.
+ * @param \WP_Theme $old_theme Instance of the old theme.
+ * @return void
+ */
+function wc_set_hooked_blocks_version_on_theme_switch( $old_name, $old_theme ) {
+ $option_name = 'woocommerce_hooked_blocks_version';
+ $option_value = get_option( $option_name, false );
+
+ // Sites with the option value set to "no" have already been migrated, and block hooks have been disabled. Checking explicitly for false to avoid setting the option again.
+ if ( ! $old_theme->is_block_theme() && ( wc_current_theme_is_fse_theme() || current_theme_supports( 'block-template-parts' ) ) && false === $option_value ) {
+ add_option( $option_name, WC()->version );
+ }
+}
diff --git a/plugins/woocommerce/includes/wc-template-hooks.php b/plugins/woocommerce/includes/wc-template-hooks.php
index 22588ca1d2f..e9fa7da964f 100644
--- a/plugins/woocommerce/includes/wc-template-hooks.php
+++ b/plugins/woocommerce/includes/wc-template-hooks.php
@@ -319,3 +319,8 @@ add_action( 'woocommerce_before_customer_login_form', 'woocommerce_output_all_no
add_action( 'woocommerce_before_lost_password_form', 'woocommerce_output_all_notices', 10 );
add_action( 'before_woocommerce_pay', 'woocommerce_output_all_notices', 10 );
add_action( 'woocommerce_before_reset_password_form', 'woocommerce_output_all_notices', 10 );
+
+/**
+ * Hooked blocks.
+ */
+add_action( 'after_switch_theme', 'wc_set_hooked_blocks_version_on_theme_switch', 10, 2 );
diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php
index db0d2fc0c99..b3604083bb8 100644
--- a/plugins/woocommerce/includes/wc-update-functions.php
+++ b/plugins/woocommerce/includes/wc-update-functions.php
@@ -2729,3 +2729,89 @@ function wc_update_891_create_plugin_autoinstall_history_option() {
function wc_update_910_add_launch_your_store_tour_option() {
add_option( 'woocommerce_show_lys_tour', 'yes' );
}
+
+/**
+ * Add woocommerce_hooked_blocks_version option for existing stores that are using a theme that supports the Block Hooks API
+ */
+function wc_update_920_add_wc_hooked_blocks_version_option() {
+ if ( ! wc_current_theme_is_fse_theme() && ! current_theme_supports( 'block-template-parts' ) ) {
+ return;
+ }
+
+ $option_name = 'woocommerce_hooked_blocks_version';
+ $option_value = get_option( $option_name );
+
+ // If the option already exists, we don't need to do anything.
+ if ( false !== $option_value ) {
+ return;
+ }
+
+ /**
+ * A list of theme slugs to execute this with.
+ * We are applying this filter to allow for the list to be extended by third-parties who were already using it.
+ *
+ * @since 8.4.0
+ */
+ $theme_include_list = apply_filters( 'woocommerce_hooked_blocks_theme_include_list', array( 'Twenty Twenty-Four', 'Twenty Twenty-Three', 'Twenty Twenty-Two', 'Tsubaki', 'Zaino', 'Thriving Artist', 'Amulet', 'Tazza' ) );
+ $active_theme_name = wp_get_theme()->get( 'Name' );
+ $should_set_hooked_blocks_version = in_array( $active_theme_name, $theme_include_list, true );
+
+ if ( $should_set_hooked_blocks_version ) {
+ // Set 8.4.0 as the version for existing stores that are using a theme that supports the Block Hooks API.
+ // This will ensure that the Block Hooks API is enabled for these stores and works as expected.
+ // Existing stores that aren't running approved block themes will not have the Block Hooks API enabled.
+ add_option( $option_name, '8.4.0' );
+ } else {
+ // For block themes that aren't approved themes set this option to "no" to completely disable hooked blocks.
+ // This means we can assume the absence of the option is when a site is switching from a classic theme to a block theme for the first time.
+ // Note: We have to use "no" instead of false since the latter is the default value for the option if it doesn't exist.
+ add_option( $option_name, 'no' );
+ }
+}
+
+/**
+ * Remove user meta associated with the keys '_last_order', '_order_count' and '_money_spent'.
+ *
+ * New keys are now used for these, to improve compatibility with multisite networks.
+ *
+ * @return void
+ */
+function wc_update_910_remove_obsolete_user_meta() {
+ global $wpdb;
+
+ $deletions = $wpdb->query( "
+ DELETE FROM $wpdb->usermeta
+ WHERE meta_key IN (
+ '_last_order',
+ '_order_count',
+ '_money_spent'
+ )
+ " );
+
+ $logger = wc_get_logger();
+
+ if ( null === $logger ) {
+ return;
+ }
+
+ if ( false === $deletions ) {
+ $logger->notice(
+ 'During the update to 9.1.0, WooCommerce attempted to remove user meta with the keys "_last_order", "_order_count" and "_money_spent" but was unable to do so.',
+ array(
+ 'source' => 'wc-updater',
+ )
+ );
+ } else {
+ $logger->info(
+ sprintf(
+ 1 === $deletions
+ ? 'During the update to 9.1.0, WooCommerce removed %d user meta row associated with the meta keys "_last_order", "_order_count" or "_money_spent".'
+ : 'During the update to 9.1.0, WooCommerce removed %d user meta rows associated with the meta keys "_last_order", "_order_count" or "_money_spent".',
+ number_format_i18n( $deletions )
+ ),
+ array(
+ 'source' => 'wc-updater',
+ )
+ );
+ }
+}
diff --git a/plugins/woocommerce/includes/wc-user-functions.php b/plugins/woocommerce/includes/wc-user-functions.php
index 06a5d732515..aa6e60f318a 100644
--- a/plugins/woocommerce/includes/wc-user-functions.php
+++ b/plugins/woocommerce/includes/wc-user-functions.php
@@ -9,6 +9,7 @@
*/
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
+use Automattic\WooCommerce\Internal\Utilities\Users;
use Automattic\WooCommerce\Utilities\OrderUtil;
defined( 'ABSPATH' ) || exit;
@@ -280,9 +281,9 @@ function wc_update_new_customer_past_orders( $customer_id ) {
if ( $complete ) {
update_user_meta( $customer_id, 'paying_customer', 1 );
- update_user_meta( $customer_id, '_order_count', '' );
- update_user_meta( $customer_id, '_money_spent', '' );
- delete_user_meta( $customer_id, '_last_order' );
+ Users::update_site_user_meta( $customer_id, 'wc_order_count', '' );
+ Users::update_site_user_meta( $customer_id, 'wc_money_spent', '' );
+ Users::delete_site_user_meta( $customer_id, 'wc_last_order' );
}
return $linked;
diff --git a/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-ssr-controller.php b/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-ssr-controller.php
index 0dbee4d8feb..886e8e286b2 100644
--- a/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-ssr-controller.php
+++ b/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-ssr-controller.php
@@ -55,7 +55,7 @@ class WC_REST_WCCOM_Site_SSR_Controller extends WC_REST_WCCOM_Site_Controller {
}
/**
- * Generate SSR data and submit it to WooCommmerce.com.
+ * Generate SSR data and submit it to WooCommerce.com.
*
* @since 7.8.0
* @param WP_REST_Request $request Full details about the request.
diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json
index 942b39e0e6c..0506a8a3dbe 100644
--- a/plugins/woocommerce/package.json
+++ b/plugins/woocommerce/package.json
@@ -2,7 +2,7 @@
"name": "@woocommerce/plugin-woocommerce",
"private": true,
"title": "WooCommerce",
- "version": "9.2.0",
+ "version": "9.3.0",
"homepage": "https://woocommerce.com/",
"repository": {
"type": "git",
@@ -51,10 +51,9 @@
"makepot": "composer run-script makepot",
"packages:fix:textdomain": "node ./bin/package-update-textdomain.js",
"test": "pnpm test:unit",
- "test:api": "API_TEST_REPORT_DIR=\"$PWD/tests/api\" pnpm exec wc-api-tests test api",
- "test:api-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js",
- "test:e2e-pw": "pnpm test:e2e:install && pnpm playwright test --config=tests/e2e-pw/envs/default/playwright.config.js",
- "test:e2e": "pnpm test:e2e:install && pnpm test:e2e:with-env default",
+ "test:api": "pnpm test:e2e:default --project=api --workers 4",
+ "test:e2e": "pnpm test:e2e:default --project=ui",
+ "test:e2e:default": "pnpm test:e2e:install && pnpm test:e2e:with-env default",
"test:e2e:install": "pnpm playwright install chromium",
"test:e2e:blocks": "pnpm --filter='@woocommerce/block-library' test:e2e",
"test:e2e:with-env": "pnpm test:e2e:install && bash ./tests/e2e-pw/run-tests-with-env.sh",
@@ -73,7 +72,8 @@
"update-wp-env": "php ./tests/e2e-pw/bin/update-wp-env.php",
"watch:build": "pnpm --if-present --workspace-concurrency=Infinity --filter=\"$npm_package_name...\" --parallel '/^watch:build:project:.*$/'",
"watch:build:project": "pnpm --if-present run '/^watch:build:project:.*$/'",
- "watch:build:project:copy-assets": "wireit"
+ "watch:build:project:copy-assets": "wireit",
+ "wp-env": "wp-env"
},
"lint-staged": {
"*.php": [
@@ -104,33 +104,13 @@
},
"tests": [
{
- "name": "PHP",
+ "name": "PHP: 8.0 WP: latest",
"testType": "unit:php",
"command": "test:php:env",
- "changes": [
- "client/admin/config/*.json",
- "composer.json",
- "composer.lock",
- "includes/**/*.php",
- "patterns/**/*.php",
- "src/**/*.php",
- "templates/**/*.php",
- "tests/php/**/*.php",
- "tests/legacy/unit-tests/**/*.php",
- "tests/unit-tests/**/*.php"
+ "shardingArguments": [
+ "--testsuite=wc-phpunit-legacy",
+ "--testsuite=wc-phpunit-main"
],
- "testEnv": {
- "start": "env:test"
- },
- "events": [
- "pull_request",
- "push"
- ]
- },
- {
- "name": "PHP 8.0",
- "testType": "unit:php",
- "command": "test:php:env",
"changes": [
"client/admin/config/*.json",
"composer.json",
@@ -141,12 +121,14 @@
"templates/**/*.php",
"tests/php/**/*.php",
"tests/legacy/unit-tests/**/*.php",
- "tests/unit-tests/**/*.php"
+ "tests/unit-tests/**/*.php",
+ ".wp-env.json"
],
"testEnv": {
"start": "env:test",
"config": {
- "phpVersion": "8.0"
+ "phpVersion": "8.0",
+ "wpVersion": "latest"
}
},
"events": [
@@ -158,6 +140,10 @@
"name": "PHP WP: latest - 1",
"testType": "unit:php",
"command": "test:php:env",
+ "shardingArguments": [
+ "--testsuite=wc-phpunit-legacy",
+ "--testsuite=wc-phpunit-main"
+ ],
"changes": [
"client/admin/config/*.json",
"composer.json",
@@ -168,7 +154,8 @@
"templates/**/*.php",
"tests/php/**/*.php",
"tests/legacy/unit-tests/**/*.php",
- "tests/unit-tests/**/*.php"
+ "tests/unit-tests/**/*.php",
+ ".wp-env.json"
],
"testEnv": {
"start": "env:test",
@@ -181,37 +168,14 @@
"push"
]
},
- {
- "name": "PHP WP: latest - 2",
- "testType": "unit:php",
- "command": "test:php:env",
- "changes": [
- "client/admin/config/*.json",
- "composer.json",
- "composer.lock",
- "includes/**/*.php",
- "patterns/**/*.php",
- "src/**/*.php",
- "templates/**/*.php",
- "tests/php/**/*.php",
- "tests/legacy/unit-tests/**/*.php",
- "tests/unit-tests/**/*.php"
- ],
- "testEnv": {
- "start": "env:test",
- "config": {
- "wpVersion": "latest-2"
- }
- },
- "events": [
- "pull_request",
- "push"
- ]
- },
{
"name": "PHP WP: nightly",
"testType": "unit:php",
"command": "test:php:env",
+ "shardingArguments": [
+ "--testsuite=wc-phpunit-legacy",
+ "--testsuite=wc-phpunit-main"
+ ],
"optional": true,
"changes": [
"client/admin/config/*.json",
@@ -223,7 +187,8 @@
"templates/**/*.php",
"tests/php/**/*.php",
"tests/legacy/unit-tests/**/*.php",
- "tests/unit-tests/**/*.php"
+ "tests/unit-tests/**/*.php",
+ ".wp-env.json"
],
"testEnv": {
"start": "env:test",
@@ -239,7 +204,7 @@
{
"name": "Core e2e tests",
"testType": "e2e",
- "command": "test:e2e:with-env default",
+ "command": "test:e2e",
"shardingArguments": [
"--shard=1/6",
"--shard=2/6",
@@ -256,7 +221,8 @@
"patterns/**/*.php",
"src/**/*.php",
"templates/**/*.php",
- "tests/e2e-pw/**"
+ "tests/e2e-pw/**",
+ ".wp-env.json"
],
"testEnv": {
"start": "env:test"
@@ -286,7 +252,6 @@
"changes": [],
"events": [
"daily-checks",
- "nightly-checks",
"release-checks"
],
"testEnv": {
@@ -383,7 +348,7 @@
{
"name": "Core e2e tests - HPOS disabled",
"testType": "e2e",
- "command": "test:e2e:with-env default",
+ "command": "test:e2e",
"shardingArguments": [
"--shard=1/5",
"--shard=2/5",
@@ -395,16 +360,7 @@
"daily-checks",
"release-checks"
],
- "changes": [
- "client/admin/config/*.json",
- "composer.json",
- "composer.lock",
- "includes/**/*.php",
- "patterns/**/*.php",
- "src/**/*.php",
- "templates/**/*.php",
- "tests/e2e-pw/**"
- ],
+ "changes": [],
"testEnv": {
"start": "env:test",
"config": {
@@ -420,7 +376,7 @@
{
"name": "Core e2e tests - PHP 8.1",
"testType": "e2e",
- "command": "test:e2e:with-env default",
+ "command": "test:e2e",
"shardingArguments": [
"--shard=1/5",
"--shard=2/5",
@@ -448,7 +404,7 @@
{
"name": "Core e2e tests - WP latest-1",
"testType": "e2e",
- "command": "test:e2e:with-env default",
+ "command": "test:e2e",
"shardingArguments": [
"--shard=1/5",
"--shard=2/5",
@@ -476,7 +432,7 @@
{
"name": "Core API tests",
"testType": "api",
- "command": "test:api-pw",
+ "command": "test:api",
"optional": false,
"changes": [
"client/admin/config/*.json",
@@ -486,8 +442,7 @@
"patterns/**/*.php",
"src/**/*.php",
"templates/**/*.php",
- "tests/api-core-tests/**",
- "tests/e2e-pw/bin/**"
+ ".wp-env.json"
],
"testEnv": {
"start": "env:test"
@@ -498,14 +453,14 @@
],
"report": {
"resultsBlobName": "core-api-report",
- "resultsPath": "tests/api-core-tests/test-results",
+ "resultsPath": "tests/e2e-pw/test-results",
"allure": true
}
},
{
"name": "Core API tests - HPOS disabled",
"testType": "api",
- "command": "test:api-pw",
+ "command": "test:api",
"optional": false,
"changes": [
"client/admin/config/*.json",
@@ -515,8 +470,8 @@
"patterns/**/*.php",
"src/**/*.php",
"templates/**/*.php",
- "tests/api-core-tests/**",
- "tests/e2e-pw/bin/**"
+ "tests/e2e-pw/bin/**",
+ ".wp-env.json"
],
"events": [
"push"
@@ -529,7 +484,7 @@
},
"report": {
"resultsBlobName": "core-api-report-hpos-disabled",
- "resultsPath": "tests/api-core-tests/test-results",
+ "resultsPath": "tests/e2e-pw/test-results",
"allure": true
}
},
@@ -546,7 +501,8 @@
"patterns/**/*.php",
"src/**/*.php",
"templates/**/*.php",
- "tests/performance/**"
+ "tests/performance/**",
+ ".wp-env.json"
],
"testEnv": {
"start": "env:perf"
@@ -569,10 +525,11 @@
"src/**/*.php",
"templates/**/*.php",
"templates/**/*.html",
- "tests/metrics/**"
+ "tests/metrics/**",
+ ".wp-env.json"
],
"events": [
- "push"
+ "disabled"
]
},
{
@@ -601,7 +558,8 @@
"events": [
"pull_request",
"push",
- "release-checks"
+ "release-checks",
+ "nightly-checks"
],
"report": {
"resultsBlobName": "blocks-e2e-report",
@@ -616,13 +574,30 @@
"shardingArguments": [],
"changes": [],
"events": [
- "daily-checks"
+ "daily-checks",
+ "on-demand"
],
"report": {
"resultsBlobName": "default-pressable-core-e2e",
"resultsPath": "tests/e2e-pw/test-results",
"allure": true
}
+ },
+ {
+ "name": "Core e2e tests - default WPCOM site",
+ "testType": "e2e",
+ "command": "test:e2e:with-env default-wpcom",
+ "shardingArguments": [],
+ "changes": [],
+ "events": [
+ "daily-checks",
+ "on-demand"
+ ],
+ "report": {
+ "resultsBlobName": "default-wpcom-core-e2e",
+ "resultsPath": "tests/e2e-pw/test-results",
+ "allure": true
+ }
}
]
}
@@ -647,7 +622,7 @@
"@wordpress/babel-plugin-import-jsx-pragma": "1.1.3",
"@wordpress/babel-preset-default": "3.0.2",
"@wordpress/e2e-test-utils-playwright": "wp-6.4",
- "@wordpress/env": "^9.0.7",
+ "@wordpress/env": "^9.7.0",
"@wordpress/stylelint-config": "^21.36.0",
"allure-commandline": "^2.25.0",
"allure-playwright": "^2.9.2",
diff --git a/plugins/woocommerce/patterns/banner.php b/plugins/woocommerce/patterns/banner.php
index 09d145e6299..11da651b411 100644
--- a/plugins/woocommerce/patterns/banner.php
+++ b/plugins/woocommerce/patterns/banner.php
@@ -27,9 +27,9 @@ $second_description = $content['descriptions'][1]['default'] ?? '';
-
-
-
+
+
+
diff --git a/plugins/woocommerce/patterns/coming-soon-entire-site.php b/plugins/woocommerce/patterns/coming-soon-entire-site.php
index e168886d453..6853b90d637 100644
--- a/plugins/woocommerce/patterns/coming-soon-entire-site.php
+++ b/plugins/woocommerce/patterns/coming-soon-entire-site.php
@@ -19,8 +19,8 @@ if ( 'twentytwentyfour' === $current_theme ) {
}
?>
-
-
diff --git a/plugins/woocommerce/patterns/coming-soon-store-only.php b/plugins/woocommerce/patterns/coming-soon-store-only.php
index b84d6e6a192..ad54ce0ccc9 100644
--- a/plugins/woocommerce/patterns/coming-soon-store-only.php
+++ b/plugins/woocommerce/patterns/coming-soon-store-only.php
@@ -20,8 +20,8 @@ if ( 'twentytwentyfour' === $current_theme ) {
?>
-
-
diff --git a/plugins/woocommerce/patterns/content-right-image-left.php b/plugins/woocommerce/patterns/content-right-image-left.php
index baa05e426d0..f6959940329 100644
--- a/plugins/woocommerce/patterns/content-right-image-left.php
+++ b/plugins/woocommerce/patterns/content-right-image-left.php
@@ -9,9 +9,9 @@ declare(strict_types=1);
use Automattic\WooCommerce\Blocks\AIContent\PatternsHelper;
-$header = __( 'Discover a world of possibilities', 'woocommerce' );
-$content = __( 'Welcome to a world of limitless possibilities, where the journey is as exhilarating as the destination, and where every moment is an opportunity to make your mark on the canvas of existence. The only limit is the extent of your imagination.', 'woocommerce' );
-$button = __( 'Get Started', 'woocommerce' );
+$header = __( 'Committed to a greener lifestyle', 'woocommerce' );
+$content = __( "Our passion is crafting mindful moments with locally sourced, organic, and sustainable products. We're more than a store; we're your path to a community-driven, eco-friendly lifestyle that embraces premium quality.", 'woocommerce' );
+$button = __( 'Meet us', 'woocommerce' );
$image_0 = PatternsHelper::get_image_url( $images, 0, 'assets/images/pattern-placeholders/drinkware-liquid-tableware-dishware-bottle-fluid.jpg' );
?>
diff --git a/plugins/woocommerce/patterns/featured-category-cover-image.php b/plugins/woocommerce/patterns/featured-category-cover-image.php
index 0b9d5b6d7ea..fbc70579df4 100644
--- a/plugins/woocommerce/patterns/featured-category-cover-image.php
+++ b/plugins/woocommerce/patterns/featured-category-cover-image.php
@@ -14,7 +14,7 @@ $description = $content['descriptions'][0]['default'] ?? '';
$button = $content['buttons'][0]['default'] ?? '';
?>
-
+
diff --git a/plugins/woocommerce/patterns/filters.php b/plugins/woocommerce/patterns/filters.php
new file mode 100644
index 00000000000..f386dfbad44
--- /dev/null
+++ b/plugins/woocommerce/patterns/filters.php
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+attribute_id;
+}
+?>
+
+
+
+
+
+
+
+
+
diff --git a/plugins/woocommerce/patterns/four-image-grid-content-left.php b/plugins/woocommerce/patterns/four-image-grid-content-left.php
index 383e5aedc5f..30e05b32f52 100644
--- a/plugins/woocommerce/patterns/four-image-grid-content-left.php
+++ b/plugins/woocommerce/patterns/four-image-grid-content-left.php
@@ -18,14 +18,12 @@ $image_3 = PatternsHelper::get_image_url( $images, 0, 'assets/images/pattern-pla
?>
-
-
-
+
-
+
diff --git a/plugins/woocommerce/patterns/header-minimal.php b/plugins/woocommerce/patterns/header-minimal.php
index 8d42656b815..589e9ad6f49 100644
--- a/plugins/woocommerce/patterns/header-minimal.php
+++ b/plugins/woocommerce/patterns/header-minimal.php
@@ -19,7 +19,10 @@
+
+
+
diff --git a/plugins/woocommerce/patterns/heading-with-three-columns-of-content-with-link.php b/plugins/woocommerce/patterns/heading-with-three-columns-of-content-with-link.php
index 10af0171142..ea0b91ac4de 100644
--- a/plugins/woocommerce/patterns/heading-with-three-columns-of-content-with-link.php
+++ b/plugins/woocommerce/patterns/heading-with-three-columns-of-content-with-link.php
@@ -20,8 +20,8 @@ $button_link = __( 'Get started', 'woocommerce' );
-
-
+
+
diff --git a/plugins/woocommerce/patterns/hero-product-3-split.php b/plugins/woocommerce/patterns/hero-product-3-split.php
index 4fb7f78c5a8..3da1d1619bc 100644
--- a/plugins/woocommerce/patterns/hero-product-3-split.php
+++ b/plugins/woocommerce/patterns/hero-product-3-split.php
@@ -50,7 +50,7 @@ $third_description = $content['descriptions'][2]['default'] ?? '';
-
+
diff --git a/plugins/woocommerce/patterns/hero-product-chessboard.php b/plugins/woocommerce/patterns/hero-product-chessboard.php
index 521f4ccf355..074fd5afc68 100644
--- a/plugins/woocommerce/patterns/hero-product-chessboard.php
+++ b/plugins/woocommerce/patterns/hero-product-chessboard.php
@@ -21,7 +21,7 @@ $third_description = $content['descriptions'][2]['default'] ?? '';
$button = $content['buttons'][0]['default'] ?? '';
?>
-
+
diff --git a/plugins/woocommerce/patterns/product-collection-4-columns.php b/plugins/woocommerce/patterns/product-collection-4-columns.php
index 5162999a59e..c74d10bdd96 100644
--- a/plugins/woocommerce/patterns/product-collection-4-columns.php
+++ b/plugins/woocommerce/patterns/product-collection-4-columns.php
@@ -8,7 +8,7 @@
$products_title = $content['titles'][0]['default'] ?? '';
?>
-
+
@@ -18,6 +18,10 @@ $products_title = $content['titles'][0]['default'] ?? '';
+
+
+
+
diff --git a/plugins/woocommerce/patterns/product-collection-5-columns.php b/plugins/woocommerce/patterns/product-collection-5-columns.php
index 8bcc01d3106..679aa7b8e49 100644
--- a/plugins/woocommerce/patterns/product-collection-5-columns.php
+++ b/plugins/woocommerce/patterns/product-collection-5-columns.php
@@ -18,6 +18,10 @@ $products_title = $content['titles'][0]['default'] ?? '';
+
+
+
+
diff --git a/plugins/woocommerce/patterns/product-collection-featured-products-5-columns.php b/plugins/woocommerce/patterns/product-collection-featured-products-5-columns.php
index 0b0af2e252b..eb4fdd8a6d4 100644
--- a/plugins/woocommerce/patterns/product-collection-featured-products-5-columns.php
+++ b/plugins/woocommerce/patterns/product-collection-featured-products-5-columns.php
@@ -20,6 +20,10 @@ $collection_title = $content['titles'][0]['default'] ?? '';
+
+
+
+
diff --git a/plugins/woocommerce/patterns/product-query-product-gallery.php b/plugins/woocommerce/patterns/product-query-product-gallery.php
index f1d7dacd549..4aa243a0ea7 100644
--- a/plugins/woocommerce/patterns/product-query-product-gallery.php
+++ b/plugins/woocommerce/patterns/product-query-product-gallery.php
@@ -19,6 +19,10 @@ $products_title = $content['titles'][0]['default'] ?? '';
+
+
+
+
diff --git a/plugins/woocommerce/patterns/social-follow-us-in-social-media.php b/plugins/woocommerce/patterns/social-follow-us-in-social-media.php
index afcfd73a82c..ce7e871f649 100644
--- a/plugins/woocommerce/patterns/social-follow-us-in-social-media.php
+++ b/plugins/woocommerce/patterns/social-follow-us-in-social-media.php
@@ -41,6 +41,10 @@ $social_title = $content['titles'][0]['default'] ?? '';
+
+
+
+
diff --git a/plugins/woocommerce/patterns/testimonials-3-columns.php b/plugins/woocommerce/patterns/testimonials-3-columns.php
index bedaf53adb1..c659a0600ad 100644
--- a/plugins/woocommerce/patterns/testimonials-3-columns.php
+++ b/plugins/woocommerce/patterns/testimonials-3-columns.php
@@ -24,6 +24,10 @@ $third_description = $content['descriptions'][2]['default'] ?? '';
+
+
+
+
@@ -37,7 +41,7 @@ $third_description = $content['descriptions'][2]['default'] ?? '';
-
~ Sophia K.
+
Sophia K.
@@ -54,7 +58,7 @@ $third_description = $content['descriptions'][2]['default'] ?? '';
-
~ Liam M.
+
Liam M.
@@ -70,7 +74,7 @@ $third_description = $content['descriptions'][2]['default'] ?? '';
-
~ Ava L.
+
Ava L.
diff --git a/plugins/woocommerce/patterns/testimonials-single.php b/plugins/woocommerce/patterns/testimonials-single.php
index 0f5df3a6c73..8b495b788d2 100644
--- a/plugins/woocommerce/patterns/testimonials-single.php
+++ b/plugins/woocommerce/patterns/testimonials-single.php
@@ -28,8 +28,8 @@ $description = $content['descriptions'][0]['default'] ?? '';
-
-
+
+
@@ -39,7 +39,7 @@ $description = $content['descriptions'][0]['default'] ?? '';
-
– Monica P.
+
Monica P.
diff --git a/plugins/woocommerce/patterns/three-columns-with-images-and-content.php b/plugins/woocommerce/patterns/three-columns-with-images-and-content.php
index c0852e91fdc..7aa31d055a6 100644
--- a/plugins/woocommerce/patterns/three-columns-with-images-and-content.php
+++ b/plugins/woocommerce/patterns/three-columns-with-images-and-content.php
@@ -23,8 +23,8 @@ $image_2 = PatternsHelper::get_image_url( $images, 0, 'assets/images/patte
-
-
+
+
diff --git a/plugins/woocommerce/phpunit.xml b/plugins/woocommerce/phpunit.xml
index 608fe200f14..9038c56f82d 100644
--- a/plugins/woocommerce/phpunit.xml
+++ b/plugins/woocommerce/phpunit.xml
@@ -9,20 +9,13 @@
verbose="true"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
-
+
./tests/legacy/unit-tests
+
+
./tests/php
./tests/php/helpers
-
- ./tests/legacy/unit-tests
- ./tests/php
- ./tests/legacy/unit-tests/woocommerce-admin
- ./tests/php/helpers
-
-
- ./tests/legacy/unit-tests/woocommerce-admin
-
diff --git a/plugins/woocommerce/readme.txt b/plugins/woocommerce/readme.txt
index 9d905836bda..16a7b9988f7 100644
--- a/plugins/woocommerce/readme.txt
+++ b/plugins/woocommerce/readme.txt
@@ -1,10 +1,10 @@
=== WooCommerce ===
Contributors: automattic, woocommerce, mikejolley, jameskoster, claudiosanches, rodrigosprimo, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, wpmuguru, royho, barryhughes-1, claudiulodro, tiagonoronha, ryelle, levinmedia, aljullu, nerrad, joshuawold, assassinateur, haszari, mppfeiffer, nielslange, opr18, ralucastn, tjcafferkey, danielwrobert, patriciahillebrandt, albarin, dinhtungdu, imanish003, karolmanijak, sunyatasattva, alexandrelara, gigitux, danieldudzic, samueljseay, alexflorisca, opr18, tarunvijwani, pauloarromba, saadtarhi, bor0, kloon, coreymckrill, jorgeatorres, leifsinger
Tags: online store, ecommerce, shop, shopping cart, sell online
-Requires at least: 6.4
-Tested up to: 6.5
+Requires at least: 6.5
+Tested up to: 6.6
Requires PHP: 7.4
-Stable tag: 9.0.2
+Stable tag: 9.1.4
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html
@@ -169,6 +169,6 @@ WooCommerce comes with some sample data you can use to see how products look; im
== Changelog ==
-= 9.2.0 2024-XX-XX =
+= 9.3.0 2024-XX-XX =
[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/changelog.txt).
diff --git a/plugins/woocommerce/src/Admin/API/OnboardingFreeExtensions.php b/plugins/woocommerce/src/Admin/API/OnboardingFreeExtensions.php
index c779f9d5c5d..3d044bab301 100644
--- a/plugins/woocommerce/src/Admin/API/OnboardingFreeExtensions.php
+++ b/plugins/woocommerce/src/Admin/API/OnboardingFreeExtensions.php
@@ -97,37 +97,6 @@ class OnboardingFreeExtensions extends WC_REST_Data_Controller {
}
}
- $extensions = $this->replace_jetpack_with_jetpack_boost_for_treatment( $extensions );
-
return new WP_REST_Response( $extensions );
}
-
- private function replace_jetpack_with_jetpack_boost_for_treatment( array $extensions ) {
- $is_treatment = \WooCommerce\Admin\Experimental_Abtest::in_treatment( 'woocommerce_jetpack_copy' );
-
- if ( ! $is_treatment ) {
- return $extensions;
- }
-
- $has_core_profiler = array_search( 'obw/core-profiler', array_column( $extensions, 'key' ) );
-
- if ( $has_core_profiler === false ) {
- return $extensions;
- }
-
- $has_jetpack = array_search( 'jetpack', array_column( $extensions[ $has_core_profiler ]['plugins'], 'key' ) );
-
- if ( $has_jetpack === false ) {
- return $extensions;
- }
-
- $jetpack = &$extensions[ $has_core_profiler ]['plugins'][ $has_jetpack ];
- $jetpack->key = 'jetpack-boost';
- $jetpack->name = 'Jetpack Boost';
- $jetpack->label = __( 'Optimize store performance with Jetpack Boost', 'woocommerce' );
- $jetpack->description = __( 'Speed up your store and improve your SEO with performance-boosting tools from Jetpack. Learn more', 'woocommerce' );
- $jetpack->learn_more_link = 'https://jetpack.com/boost/';
-
- return $extensions;
- }
}
diff --git a/plugins/woocommerce/src/Admin/API/Orders.php b/plugins/woocommerce/src/Admin/API/Orders.php
index 64138466a65..863c377afa4 100644
--- a/plugins/woocommerce/src/Admin/API/Orders.php
+++ b/plugins/woocommerce/src/Admin/API/Orders.php
@@ -239,7 +239,7 @@ class Orders extends \WC_REST_Orders_Controller {
}
// Format the order status.
- $data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];
+ $data['status'] = OrderUtil::remove_status_prefix( $data['status'] );
// Format requested line items.
$formatted_line_items = array();
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Orders/Controller.php b/plugins/woocommerce/src/Admin/API/Reports/Orders/Controller.php
index 81e85182f1c..b48a7245d38 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Orders/Controller.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Orders/Controller.php
@@ -524,14 +524,13 @@ class Controller extends ReportsController implements ExportableInterface {
$export_columns = array(
'date_created' => __( 'Date', 'woocommerce' ),
'order_number' => __( 'Order #', 'woocommerce' ),
- 'total_formatted' => __( 'N. Revenue (formatted)', 'woocommerce' ),
'status' => __( 'Status', 'woocommerce' ),
'customer_name' => __( 'Customer', 'woocommerce' ),
'customer_type' => __( 'Customer type', 'woocommerce' ),
'products' => __( 'Product(s)', 'woocommerce' ),
'num_items_sold' => __( 'Items sold', 'woocommerce' ),
'coupons' => __( 'Coupon(s)', 'woocommerce' ),
- 'net_total' => __( 'N. Revenue', 'woocommerce' ),
+ 'net_total' => __( 'Net Sales', 'woocommerce' ),
'attribution' => __( 'Attribution', 'woocommerce' ),
);
@@ -555,16 +554,15 @@ class Controller extends ReportsController implements ExportableInterface {
*/
public function prepare_item_for_export( $item ) {
$export_item = array(
- 'date_created' => $item['date_created'],
+ 'date_created' => $item['date'],
'order_number' => $item['order_number'],
- 'total_formatted' => $item['total_formatted'],
'status' => $item['status'],
'customer_name' => isset( $item['extended_info']['customer'] ) ? $this->get_customer_name( $item['extended_info']['customer'] ) : null,
'customer_type' => $item['customer_type'],
'products' => isset( $item['extended_info']['products'] ) ? $this->get_products( $item['extended_info']['products'] ) : null,
'num_items_sold' => $item['num_items_sold'],
'coupons' => isset( $item['extended_info']['coupons'] ) ? $this->get_coupons( $item['extended_info']['coupons'] ) : null,
- 'net_total' => $item['net_total'],
+ 'net_total' => $item['net_total'],
'attribution' => $item['extended_info']['attribution']['origin'],
);
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Taxes/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Taxes/DataStore.php
index b5e5eb94587..2f9a430e7cb 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Taxes/DataStore.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Taxes/DataStore.php
@@ -82,7 +82,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
'total_tax' => 'SUM(total_tax) as total_tax',
'order_tax' => 'SUM(order_tax) as order_tax',
'shipping_tax' => 'SUM(shipping_tax) as shipping_tax',
- 'orders_count' => "COUNT({$table_name}.order_id) as orders_count",
+ 'orders_count' => "COUNT( DISTINCT ( CASE WHEN parent_id = 0 THEN {$table_name}.order_id END ) ) as orders_count",
);
}
@@ -146,8 +146,6 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
- $table_name = self::get_db_table_name();
-
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
@@ -204,6 +202,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
$this->subquery->add_sql_clause( 'group_by', ", {$wpdb->prefix}woocommerce_order_items.order_item_name, {$wpdb->prefix}woocommerce_order_itemmeta.meta_value" );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
+ $this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$taxes_query = $this->subquery->get_query_statement();
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Variations/Controller.php b/plugins/woocommerce/src/Admin/API/Reports/Variations/Controller.php
index 39980256db7..03231e9674d 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Variations/Controller.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Variations/Controller.php
@@ -39,6 +39,7 @@ class Controller extends ReportsController implements ExportableInterface {
*/
protected $param_mapping = array(
'variations' => 'variation_includes',
+ 'products' => 'product_includes',
);
/**
@@ -382,6 +383,15 @@ class Controller extends ReportsController implements ExportableInterface {
'sanitize_callback' => 'wp_validate_boolean',
'validate_callback' => 'rest_validate_request_arg',
);
+ $params['products'] = array(
+ 'description' => __( 'Limit result to items with specified product ids.', 'woocommerce' ),
+ 'type' => 'array',
+ 'sanitize_callback' => 'wp_parse_id_list',
+ 'validate_callback' => 'rest_validate_request_arg',
+ 'items' => array(
+ 'type' => 'integer',
+ ),
+ );
return $params;
}
diff --git a/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/DefaultMarketingRecommendations.php b/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/DefaultMarketingRecommendations.php
index 25f1626d6dc..c46decb0f7f 100644
--- a/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/DefaultMarketingRecommendations.php
+++ b/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/DefaultMarketingRecommendations.php
@@ -56,7 +56,7 @@ class DefaultMarketingRecommendations {
return array(
array(
- 'title' => 'Google Listings and Ads',
+ 'title' => 'Google for WooCommerce',
'description' => __( 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', 'woocommerce' ),
'url' => "https://woocommerce.com/products/google-listings-and-ads/{$utm_string}",
'direct_install' => true,
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php
index 432bd4e20e4..335d8a37ed9 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php
@@ -4,6 +4,7 @@ namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Jetpack_Gutenberg;
+use WP_Post;
/**
* Customize Your Store Task
@@ -24,23 +25,24 @@ class CustomizeStore extends Task {
// Hook to remove unwanted UI elements when users are viewing with ?cys-hide-admin-bar=true.
add_action( 'wp_head', array( $this, 'possibly_remove_unwanted_ui_elements' ) );
- add_action( 'save_post_wp_global_styles', array( $this, 'mark_task_as_complete' ), 10, 3 );
- add_action( 'save_post_wp_template', array( $this, 'mark_task_as_complete' ), 10, 3 );
- add_action( 'save_post_wp_template_part', array( $this, 'mark_task_as_complete' ), 10, 3 );
+ add_action( 'save_post_wp_global_styles', array( $this, 'mark_task_as_complete_block_theme' ), 10, 3 );
+ add_action( 'save_post_wp_template', array( $this, 'mark_task_as_complete_block_theme' ), 10, 3 );
+ add_action( 'save_post_wp_template_part', array( $this, 'mark_task_as_complete_block_theme' ), 10, 3 );
+ add_action( 'customize_save_after', array( $this, 'mark_task_as_complete_classic_theme' ) );
}
/**
* Mark the CYS task as complete whenever the user updates their global styles.
*
- * @param int $post_id Post ID.
- * @param \WP_Post $post Post object.
- * @param bool $update Whether this is an existing post being updated.
+ * @param int $post_id Post ID.
+ * @param WP_Post $post Post object.
+ * @param bool $update Whether this is an existing post being updated.
*
* @return void
*/
- public function mark_task_as_complete( $post_id, $post, $update ) {
- if ( $post instanceof \WP_Post ) {
- $is_cys_complete = '{"version": 2, "isGlobalStylesUserThemeJSON": true }' !== $post->post_content || in_array( $post->post_type, array( 'wp_template', 'wp_template_part' ), true );
+ public function mark_task_as_complete_block_theme( $post_id, $post, $update ) {
+ if ( $post instanceof WP_Post ) {
+ $is_cys_complete = $this->has_custom_global_styles( $post ) || $this->has_custom_template( $post );
if ( $is_cys_complete ) {
update_option( 'woocommerce_admin_customize_store_completed', 'yes' );
@@ -48,6 +50,15 @@ class CustomizeStore extends Task {
}
}
+ /**
+ * Mark the CYS task as complete whenever the user saves the customizer changes.
+ *
+ * @return void
+ */
+ public function mark_task_as_complete_classic_theme() {
+ update_option( 'woocommerce_admin_customize_store_completed', 'yes' );
+ }
+
/**
* ID.
*
@@ -260,4 +271,33 @@ class CustomizeStore extends Task {
';
}
}
+
+ /**
+ * Checks if the post has custom global styles stored (if it is different from the default global styles).
+ *
+ * @param WP_Post $post The post object.
+ * @return bool
+ */
+ private function has_custom_global_styles( WP_Post $post ) {
+ $required_keys = array( 'version', 'isGlobalStylesUserThemeJSON' );
+
+ $json_post_content = json_decode( $post->post_content, true );
+ if ( is_null( $json_post_content ) ) {
+ return false;
+ }
+
+ $post_content_keys = array_keys( $json_post_content );
+
+ return ! empty( array_diff( $post_content_keys, $required_keys ) ) || ! empty( array_diff( $required_keys, $post_content_keys ) );
+ }
+
+ /**
+ * Checks if the post is a template or a template part.
+ *
+ * @param WP_Post $post The post object.
+ * @return bool Whether the post is a template or a template part.
+ */
+ private function has_custom_template( WP_Post $post ) {
+ return in_array( $post->post_type, array( 'wp_template', 'wp_template_part' ), true );
+ }
}
diff --git a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php
index 001f3ae30dd..d80271c985d 100644
--- a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php
+++ b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php
@@ -65,6 +65,13 @@ class DefaultPaymentGateways {
'CA',
)
),
+ (object) array(
+ 'type' => 'or',
+ 'operands' => array(
+ self::get_rules_for_wcpay_activated( false ),
+ self::get_rules_for_wcpay_connected( false ),
+ ),
+ ),
),
'category_other' => array(),
'category_additional' => array(
@@ -87,6 +94,13 @@ class DefaultPaymentGateways {
'AU',
)
),
+ (object) array(
+ 'type' => 'or',
+ 'operands' => array(
+ self::get_rules_for_wcpay_activated( false ),
+ self::get_rules_for_wcpay_connected( false ),
+ ),
+ ),
),
'category_other' => array(),
'category_additional' => array(
@@ -250,6 +264,19 @@ class DefaultPaymentGateways {
)
),
self::get_rules_for_cbd( false ),
+ (object) array(
+ 'type' => 'or',
+ 'operands' => array(
+ (object) array(
+ 'type' => 'not',
+ 'operand' => array(
+ self::get_rules_for_countries( self::get_wcpay_countries() ),
+ ),
+ ),
+ self::get_rules_for_wcpay_activated( false ),
+ self::get_rules_for_wcpay_connected( false ),
+ ),
+ ),
),
'category_other' => array(),
'category_additional' => array(
@@ -862,6 +889,47 @@ class DefaultPaymentGateways {
),
),
),
+ array(
+ 'id' => 'woocommerce_payments:bnpl',
+ 'title' => __( 'Activate BNPL instantly on WooPayments', 'woocommerce' ),
+ 'content' => __(
+ 'The world’s favorite buy now, pay later options and many more are right at your fingertips with WooPayments — all from one dashboard, without needing multiple extensions and logins.',
+ 'woocommerce'
+ ),
+ 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay-bnpl.svg',
+ 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay-bnpl.svg',
+ 'plugins' => array( 'woocommerce-payments' ),
+ 'is_visible' => array(
+ self::get_rules_for_countries(
+ array_intersect(
+ array(
+ 'US',
+ 'CA',
+ 'AU',
+ 'AT',
+ 'BE',
+ 'CH',
+ 'DK',
+ 'ES',
+ 'FI',
+ 'FR',
+ 'DE',
+ 'GB',
+ 'IT',
+ 'NL',
+ 'NO',
+ 'PL',
+ 'SE',
+ 'NZ',
+ ),
+ self::get_wcpay_countries()
+ ),
+ ),
+ self::get_rules_for_cbd( false ),
+ self::get_rules_for_wcpay_activated( true ),
+ self::get_rules_for_wcpay_connected( true ),
+ ),
+ ),
array(
'id' => 'zipmoney',
'title' => __( 'Zip Co - Buy Now, Pay Later', 'woocommerce' ),
@@ -976,7 +1044,7 @@ class DefaultPaymentGateways {
* Get default rules for CBD based on given argument.
*
* @param bool $should_have Whether or not the store should have CBD as an industry (true) or not (false).
- * @return array Rules to match.
+ * @return object Rules to match.
*/
public static function get_rules_for_cbd( $should_have ) {
return (object) array(
@@ -1002,6 +1070,62 @@ class DefaultPaymentGateways {
);
}
+ /**
+ * Get default rules for the WooPayments plugin being installed and activated.
+ *
+ * @param bool $should_be Whether WooPayments should be activated.
+ *
+ * @return object Rules to match.
+ */
+ public static function get_rules_for_wcpay_activated( $should_be ) {
+ $active_rule = (object) array(
+ 'type' => 'plugins_activated',
+ 'plugins' => array( 'woocommerce-payments' ),
+ );
+
+ if ( $should_be ) {
+ return $active_rule;
+ }
+
+ return (object) array(
+ 'type' => 'not',
+ 'operand' => array( $active_rule ),
+ );
+ }
+
+ /**
+ * Get default rules for WooPayments being connected or not.
+ *
+ * This does not include the check for the WooPayments plugin to be active.
+ *
+ * @param bool $should_be Whether WooPayments should be connected.
+ *
+ * @return object Rules to match.
+ */
+ public static function get_rules_for_wcpay_connected( $should_be ) {
+ return (object) array(
+ 'type' => 'option',
+ 'transformers' => array(
+ // Extract only the 'data' key from the option.
+ (object) array(
+ 'use' => 'dot_notation',
+ 'arguments' => (object) array(
+ 'path' => 'data',
+ ),
+ ),
+ // Extract the keys from the data array.
+ (object) array(
+ 'use' => 'array_keys',
+ ),
+ ),
+ 'option_name' => 'wcpay_account_data',
+ // The rule will be look for the 'account_id' key in the account data array.
+ 'operation' => $should_be ? 'contains' : '!contains',
+ 'value' => 'account_id',
+ 'default' => array(),
+ );
+ }
+
/**
* Get recommendation priority for a given payment gateway by id and country.
* If country is not supported, return null.
@@ -1013,6 +1137,8 @@ class DefaultPaymentGateways {
private static function get_recommendation_priority( $gateway_id, $country_code ) {
$recommendation_priority_map = array(
'US' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1023,6 +1149,8 @@ class DefaultPaymentGateways {
'klarna_payments',
),
'CA' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1032,6 +1160,8 @@ class DefaultPaymentGateways {
'klarna_payments',
),
'AT' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1041,6 +1171,8 @@ class DefaultPaymentGateways {
'amazon_payments_advanced',
),
'BE' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1049,26 +1181,63 @@ class DefaultPaymentGateways {
'klarna_payments',
'amazon_payments_advanced',
),
- 'BG' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
- 'HR' => array( 'woocommerce_payments', 'ppcp-gateway' ),
+ 'BG' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ ),
+ 'HR' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'ppcp-gateway',
+ ),
'CH' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'mollie_wc_gateway_banktransfer',
'klarna_payments',
),
- 'CY' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ),
- 'CZ' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
+ 'CY' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ 'amazon_payments_advanced',
+ ),
+ 'CZ' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ ),
'DK' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'klarna_payments',
'amazon_payments_advanced',
),
- 'EE' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'airwallex_main' ),
+ 'EE' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ 'airwallex_main',
+ ),
'ES' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1078,6 +1247,8 @@ class DefaultPaymentGateways {
'amazon_payments_advanced',
),
'FI' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1086,6 +1257,8 @@ class DefaultPaymentGateways {
'klarna_payments',
),
'FR' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1096,6 +1269,8 @@ class DefaultPaymentGateways {
'amazon_payments_advanced',
),
'DE' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1105,6 +1280,8 @@ class DefaultPaymentGateways {
'amazon_payments_advanced',
),
'GB' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1114,9 +1291,25 @@ class DefaultPaymentGateways {
'klarna_payments',
'amazon_payments_advanced',
),
- 'GR' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'airwallex_main' ),
- 'HU' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ),
+ 'GR' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ 'airwallex_main',
+ ),
+ 'HU' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ 'amazon_payments_advanced',
+ ),
'IE' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1125,6 +1318,8 @@ class DefaultPaymentGateways {
'amazon_payments_advanced',
),
'IT' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1133,11 +1328,38 @@ class DefaultPaymentGateways {
'klarna_payments',
'amazon_payments_advanced',
),
- 'LV' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
- 'LT' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
- 'LU' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ),
- 'MT' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
+ 'LV' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ ),
+ 'LT' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ ),
+ 'LU' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ 'amazon_payments_advanced',
+ ),
+ 'MT' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ ),
'NL' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1146,8 +1368,18 @@ class DefaultPaymentGateways {
'klarna_payments',
'amazon_payments_advanced',
),
- 'NO' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'kco', 'klarna_payments' ),
+ 'NO' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ 'kco',
+ 'klarna_payments',
+ ),
'PL' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1156,16 +1388,39 @@ class DefaultPaymentGateways {
'klarna_payments',
),
'PT' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'airwallex_main',
'amazon_payments_advanced',
),
- 'RO' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
- 'SK' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
- 'SL' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ),
+ 'RO' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ ),
+ 'SK' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ ),
+ 'SL' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'ppcp-gateway',
+ 'amazon_payments_advanced',
+ ),
'SE' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1194,6 +1449,8 @@ class DefaultPaymentGateways {
'UY' => array( 'woo-mercado-pago-custom', 'ppcp-gateway' ),
'VE' => array( 'ppcp-gateway' ),
'AU' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'airwallex_main',
@@ -1203,6 +1460,8 @@ class DefaultPaymentGateways {
'klarna_payments',
),
'NZ' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'airwallex_main',
@@ -1210,6 +1469,8 @@ class DefaultPaymentGateways {
'klarna_payments',
),
'HK' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'airwallex_main',
@@ -1217,13 +1478,22 @@ class DefaultPaymentGateways {
'payoneer-checkout',
),
'JP' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'square_credit_card',
'amazon_payments_advanced',
),
- 'SG' => array( 'woocommerce_payments', 'stripe', 'airwallex_main', 'ppcp-gateway' ),
+ 'SG' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ 'stripe',
+ 'airwallex_main',
+ 'ppcp-gateway',
+ ),
'CN' => array( 'airwallex_main', 'ppcp-gateway', 'payoneer-checkout' ),
'FJ' => array(),
'GU' => array(),
@@ -1232,7 +1502,11 @@ class DefaultPaymentGateways {
'ZA' => array( 'payfast', 'paystack' ),
'NG' => array( 'paystack' ),
'GH' => array( 'paystack' ),
- 'AE' => array( 'woocommerce_payments' ),
+ 'AE' => array(
+ 'woocommerce_payments:with-in-person-payments',
+ 'woocommerce_payments:without-in-person-payments',
+ 'woocommerce_payments',
+ ),
);
// If the country code is not in the list, return default priority.
diff --git a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php
index 662bbf5631c..45d609da940 100644
--- a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php
+++ b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php
@@ -16,15 +16,33 @@ class EvaluateSuggestion {
/**
* Evaluates the spec and returns the suggestion.
*
- * @param object|array $spec The suggestion to evaluate.
+ * @param object|array $spec The suggestion to evaluate.
+ * @param array $logger_args Optional. Arguments for the rule evaluator logger.
+ *
* @return object The evaluated suggestion.
*/
- public static function evaluate( $spec ) {
+ public static function evaluate( $spec, $logger_args = array() ) {
$rule_evaluator = new RuleEvaluator();
$suggestion = is_array( $spec ) ? (object) $spec : clone $spec;
if ( isset( $suggestion->is_visible ) ) {
- $is_visible = $rule_evaluator->evaluate( $suggestion->is_visible );
+ // Determine the suggestion's logger slug.
+ $logger_slug = ! empty( $suggestion->id ) ? $suggestion->id : '';
+ // If the suggestion has no ID, use the title to generate a slug.
+ if ( empty( $logger_slug ) ) {
+ $logger_slug = ! empty( $suggestion->title ) ? sanitize_title_with_dashes( trim( $suggestion->title ) ) : 'anonymous-suggestion';
+ }
+
+ // Evaluate the visibility of the suggestion.
+ $is_visible = $rule_evaluator->evaluate(
+ $suggestion->is_visible,
+ null,
+ array(
+ 'slug' => $logger_slug,
+ 'source' => $logger_args['source'] ?? 'wc-payment-gateway-suggestions',
+ )
+ );
+
$suggestion->is_visible = $is_visible;
}
@@ -35,15 +53,17 @@ class EvaluateSuggestion {
* Evaluates the specs and returns the visible suggestions.
*
* @param array $specs payment suggestion spec array.
+ * @param array $logger_args Optional. Arguments for the rule evaluator logger.
+ *
* @return array The visible suggestions and errors.
*/
- public static function evaluate_specs( $specs ) {
+ public static function evaluate_specs( $specs, $logger_args = array() ) {
$suggestions = array();
$errors = array();
foreach ( $specs as $spec ) {
try {
- $suggestion = self::evaluate( $spec );
+ $suggestion = self::evaluate( $spec, $logger_args );
if ( ! property_exists( $suggestion, 'is_visible' ) || $suggestion->is_visible ) {
$suggestions[] = $suggestion;
}
diff --git a/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php b/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php
index d9562e9a3e2..d31f8cf0e04 100644
--- a/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php
+++ b/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php
@@ -127,7 +127,7 @@ class Init {
$editor_settings = $this->get_product_editor_settings();
$script_handle = 'wc-admin-edit-product';
- wp_register_script( $script_handle, '', array(), '0.1.0', true );
+ wp_register_script( $script_handle, '', array( 'wp-blocks' ), '0.1.0', true );
wp_enqueue_script( $script_handle );
wp_add_inline_script(
$script_handle,
diff --git a/plugins/woocommerce/src/Admin/Features/ShippingPartnerSuggestions/ShippingPartnerSuggestions.php b/plugins/woocommerce/src/Admin/Features/ShippingPartnerSuggestions/ShippingPartnerSuggestions.php
index 19c2893252e..d309feddb17 100644
--- a/plugins/woocommerce/src/Admin/Features/ShippingPartnerSuggestions/ShippingPartnerSuggestions.php
+++ b/plugins/woocommerce/src/Admin/Features/ShippingPartnerSuggestions/ShippingPartnerSuggestions.php
@@ -20,14 +20,14 @@ class ShippingPartnerSuggestions extends RemoteSpecsEngine {
$locale = get_user_locale();
$specs = is_array( $specs ) ? $specs : self::get_specs();
- $results = EvaluateSuggestion::evaluate_specs( $specs );
+ $results = EvaluateSuggestion::evaluate_specs( $specs, array( 'source' => 'wc-shipping-partner-suggestions' ) );
$specs_to_return = $results['suggestions'];
$specs_to_save = null;
if ( empty( $specs_to_return ) ) {
// When suggestions is empty, replace it with defaults and save for 3 hours.
$specs_to_save = DefaultShippingPartners::get_all();
- $specs_to_return = EvaluateSuggestion::evaluate_specs( $specs_to_save )['suggestions'];
+ $specs_to_return = EvaluateSuggestion::evaluate_specs( $specs_to_save, array( 'source' => 'wc-shipping-partner-suggestions' ) )['suggestions'];
} elseif ( count( $results['errors'] ) > 0 ) {
// When suggestions is not empty but has errors, save it for 3 hours.
$specs_to_save = $specs;
diff --git a/plugins/woocommerce/src/Admin/PluginsHelper.php b/plugins/woocommerce/src/Admin/PluginsHelper.php
index 5d75c51dcf1..238d1cde650 100644
--- a/plugins/woocommerce/src/Admin/PluginsHelper.php
+++ b/plugins/woocommerce/src/Admin/PluginsHelper.php
@@ -43,6 +43,11 @@ class PluginsHelper {
*/
const WOO_SUBSCRIPTION_PAGE_URL = 'https://woocommerce.com/my-account/my-subscriptions/';
+ /**
+ * The URL for the WooCommerce.com add payment method page.
+ */
+ const WOO_ADD_PAYMENT_METHOD_URL = 'https://woocommerce.com/my-account/add-payment-method/';
+
/**
* Meta key for dismissing expired subscription notices.
*/
@@ -706,7 +711,7 @@ class PluginsHelper {
}
/**
- * Construct the subscritpion notice data based on user subscriptions data.
+ * Construct the subscription notice data based on user subscriptions data.
*
* @param array $all_subs all subscription data.
* @param array $subs_to_show filtered subscriptions as condition.
@@ -717,10 +722,19 @@ class PluginsHelper {
*/
public static function get_subscriptions_notice_data( array $all_subs, array $subs_to_show, int $total, array $messages, string $type ) {
if ( 1 < $total ) {
+ $hyperlink_url = add_query_arg(
+ array(
+ 'utm_source' => 'pu',
+ 'utm_campaign' => 'expired' === $type ? 'pu_settings_screen_renew' : 'pu_settings_screen_enable_autorenew',
+
+ ),
+ self::WOO_SUBSCRIPTION_PAGE_URL
+ );
+
$parsed_message = sprintf(
$messages['different_subscriptions'],
esc_attr( $total ),
- esc_url( self::WOO_SUBSCRIPTION_PAGE_URL ),
+ esc_url( $hyperlink_url ),
esc_attr( $total ),
);
@@ -752,8 +766,11 @@ class PluginsHelper {
$expiry_date = date_i18n( 'F jS', $subscription['expires'] );
$hyperlink_url = add_query_arg(
array(
- 'product_id' => $product_id,
- 'type' => $type,
+ 'product_id' => $product_id,
+ 'type' => $type,
+ 'utm_source' => 'pu',
+ 'utm_campaign' => 'expired' === $type ? 'pu_settings_screen_renew' : 'pu_settings_screen_enable_autorenew',
+
),
self::WOO_SUBSCRIPTION_PAGE_URL
);
@@ -810,6 +827,7 @@ class PluginsHelper {
$subscriptions,
function ( $sub ) {
return ( ! empty( $sub['local']['installed'] ) && ! empty( $sub['product_key'] ) )
+ && $sub['active']
&& $sub['expiring']
&& ! $sub['autorenew'];
},
@@ -824,29 +842,7 @@ class PluginsHelper {
// When payment method is missing on WooCommerce.com.
$helper_notices = WC_Helper::get_notices();
if ( ! empty( $helper_notices['missing_payment_method_notice'] ) ) {
- $description = $allowed_link
- ? sprintf(
- /* translators: %s: WooCommerce.com URL to add payment method */
- _n(
- 'Your WooCommerce extension subscription is missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
- 'Your WooCommerce extension subscriptions are missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
- $total_expiring_subscriptions,
- 'woocommerce'
- ),
- 'https://woocommerce.com/my-account/add-payment-method/'
- )
- : _n(
- 'Your WooCommerce extension subscription is missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
- 'Your WooCommerce extension subscriptions are missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
- $total_expiring_subscriptions,
- 'woocommerce'
- );
-
- return array(
- 'description' => $description,
- 'button_text' => __( 'Add payment method', 'woocommerce' ),
- 'button_link' => 'https://woocommerce.com/my-account/add-payment-method/',
- );
+ return self::get_missing_payment_method_notice( $allowed_link, $total_expiring_subscriptions );
}
// Payment method is available but there are expiring subscriptions.
@@ -865,14 +861,20 @@ class PluginsHelper {
'expiring',
);
- $button_link = self::WOO_SUBSCRIPTION_PAGE_URL;
+ $button_link = add_query_arg(
+ array(
+ 'utm_source' => 'pu',
+ 'utm_campaign' => 'pu_in_apps_screen_enable_autorenew',
+ ),
+ self::WOO_SUBSCRIPTION_PAGE_URL
+ );
if ( in_array( $notice_data['type'], array( 'single_manage', 'multiple_manage' ), true ) ) {
$button_link = add_query_arg(
array(
'product_id' => $notice_data['product_id'],
'type' => 'expiring',
),
- self::WOO_SUBSCRIPTION_PAGE_URL
+ $button_link
);
}
@@ -903,6 +905,7 @@ class PluginsHelper {
$subscriptions,
function ( $sub ) {
return ( ! empty( $sub['local']['installed'] ) && ! empty( $sub['product_key'] ) )
+ && $sub['active']
&& $sub['expired']
&& ! $sub['lifetime'];
},
@@ -923,21 +926,28 @@ class PluginsHelper {
/* translators: 1) product name 3) URL to My Subscriptions page 4) Renew product price string */
'single_manage' => __( 'Your subscription for %1$s expired. %4$s to continue receiving updates and streamlined support.', 'woocommerce' ),
/* translators: 1) product name 3) URL to My Subscriptions page 4) Renew product price string */
- 'multiple_manage' => __( 'One of your subscriptions for %1$s has expired. %4$s. to continue receiving updates and streamlined support.', 'woocommerce' ),
+ 'multiple_manage' => __( 'One of your subscriptions for %1$s has expired. %4$s to continue receiving updates and streamlined support.', 'woocommerce' ),
/* translators: 1) total expired subscriptions 2) URL to My Subscriptions page */
'different_subscriptions' => __( 'You have %1$s Woo extension subscriptions that expired. Renew to continue receiving updates and streamlined support.', 'woocommerce' ),
),
'expired',
);
- $button_link = self::WOO_SUBSCRIPTION_PAGE_URL;
+ $button_link = add_query_arg(
+ array(
+ 'utm_source' => 'pu',
+ 'utm_campaign' => $allowed_link ? 'pu_settings_screen_renew' : 'pu_in_apps_screen_renew',
+ ),
+ self::WOO_SUBSCRIPTION_PAGE_URL
+ );
+
if ( in_array( $notice_data['type'], array( 'single_manage', 'multiple_manage' ), true ) ) {
$button_link = add_query_arg(
array(
'product_id' => $notice_data['product_id'],
'type' => 'expiring',
),
- self::WOO_SUBSCRIPTION_PAGE_URL
+ $button_link
);
}
@@ -973,4 +983,45 @@ class PluginsHelper {
return true;
}
+
+ /**
+ * Get the notice data for missing payment method.
+ *
+ * @param bool $allowed_link whether should show link on the notice or not.
+ * @param int $total_expiring_subscriptions total expiring subscriptions.
+ *
+ * @return array the notices data.
+ */
+ public static function get_missing_payment_method_notice( $allowed_link = true, $total_expiring_subscriptions = 1 ) {
+ $add_payment_method_link = add_query_arg(
+ array(
+ 'utm_source' => 'pu',
+ 'utm_campaign' => $allowed_link ? 'pu_settings_screen_add_payment_method' : 'pu_in_apps_screen_add_payment_method',
+ ),
+ self::WOO_ADD_PAYMENT_METHOD_URL
+ );
+ $description = $allowed_link
+ ? sprintf(
+ /* translators: %s: WooCommerce.com URL to add payment method */
+ _n(
+ 'Your WooCommerce extension subscription is missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
+ 'Your WooCommerce extension subscriptions are missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
+ $total_expiring_subscriptions,
+ 'woocommerce'
+ ),
+ $add_payment_method_link
+ )
+ : _n(
+ 'Your WooCommerce extension subscription is missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
+ 'Your WooCommerce extension subscriptions are missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
+ $total_expiring_subscriptions,
+ 'woocommerce'
+ );
+
+ return array(
+ 'description' => $description,
+ 'button_text' => __( 'Add payment method', 'woocommerce' ),
+ 'button_link' => $add_payment_method_link,
+ );
+ }
}
diff --git a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php
index 8555ff9ba18..595a7788053 100644
--- a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php
+++ b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php
@@ -7,11 +7,14 @@ namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;
defined( 'ABSPATH' ) || exit;
+use Automattic\WooCommerce\Admin\Features\Features;
+use Automattic\WooCommerce\Admin\Notes\Notes;
use Automattic\WooCommerce\Admin\PluginsProvider\PluginsProvider;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\StoredStateSetupForProducts;
+use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
/**
* Remote Inbox Notifications engine.
@@ -19,11 +22,15 @@ use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\StoredStateSetupForP
* specs that are able to be triggered.
*/
class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
+ use AccessiblePrivateMethods;
+
const STORED_STATE_OPTION_NAME = 'wc_remote_inbox_notifications_stored_state';
const WCA_UPDATED_OPTION_NAME = 'wc_remote_inbox_notifications_wca_updated';
/**
* Initialize the engine.
+ * phpcs:disable WooCommerce.Functions.InternalInjectionMethod.MissingFinal
+ * phpcs:disable WooCommerce.Functions.InternalInjectionMethod.MissingInternalTag
*/
public static function init() {
// Init things that need to happen before admin_init.
@@ -42,10 +49,13 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
// Hook into WCA updated. This is hooked up here rather than in
// on_admin_init because that runs too late to hook into the action.
- add_action( 'woocommerce_run_on_woocommerce_admin_updated', array( __CLASS__, 'run_on_woocommerce_admin_updated' ) );
+ add_action(
+ 'woocommerce_run_on_woocommerce_admin_updated',
+ array( __CLASS__, 'run_on_woocommerce_admin_updated' )
+ );
add_action(
'woocommerce_updated',
- function() {
+ function () {
$next_hook = WC()->queue()->get_next(
'woocommerce_run_on_woocommerce_admin_updated',
array(),
@@ -63,6 +73,11 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
);
add_filter( 'woocommerce_get_note_from_db', array( __CLASS__, 'get_note_from_db' ), 10, 1 );
+ self::add_filter( 'woocommerce_debug_tools', array( __CLASS__, 'add_debug_tools' ) );
+ self::add_action(
+ 'wp_ajax_woocommerce_json_inbox_notifications_search',
+ array( __CLASS__, 'ajax_action_inbox_notification_search' )
+ );
}
/**
@@ -155,7 +170,7 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
public static function get_stored_state() {
$stored_state = get_option( self::STORED_STATE_OPTION_NAME );
- if ( $stored_state === false ) {
+ if ( false === $stored_state ) {
$stored_state = new \stdClass();
$stored_state = StoredStateSetupForProducts::init_stored_state(
@@ -199,6 +214,7 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
* Get the note. This is used to display localized note.
*
* @param Note $note_from_db The note object created from db.
+ *
* @return Note The note.
*/
public static function get_note_from_db( $note_from_db ) {
@@ -211,7 +227,7 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
continue;
}
$locale = SpecRunner::get_locale( $spec->locales, true );
- if ( $locale === null ) {
+ if ( null === $locale ) {
// No locale found, so don't update the note.
break;
}
@@ -233,4 +249,94 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
return $note_from_db;
}
+
+ /**
+ * Add the debug tools to the WooCommerce debug tools (WooCommerce > Status > Tools).
+ *
+ * @param array $tools a list of tools.
+ *
+ * @return mixed
+ */
+ private static function add_debug_tools( $tools ) {
+ // Check if the feature flag is disabled.
+ if ( ! Features::is_enabled( 'remote-inbox-notifications' ) ) {
+ return false;
+ }
+
+ // Check if the site has opted out of marketplace suggestions.
+ if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) !== 'yes' ) {
+ return false;
+ }
+
+ $tools['refresh_remote_inbox_notifications'] = array(
+ 'name' => __( 'Refresh Remote Inbox Notifications', 'woocommerce' ),
+ 'button' => __( 'Refresh', 'woocommerce' ),
+ 'desc' => __( 'This will refresh the remote inbox notifications', 'woocommerce' ),
+ 'callback' => function () {
+ RemoteInboxNotificationsDataSourcePoller::get_instance()->read_specs_from_data_sources();
+ RemoteInboxNotificationsEngine::run();
+
+ return __( 'Remote inbox notifications have been refreshed', 'woocommerce' );
+ },
+ );
+
+ $tools['delete_inbox_notification'] = array(
+ 'name' => __( 'Delete an Inbox Notification', 'woocommerce' ),
+ 'button' => __( 'Delete', 'woocommerce' ),
+ 'desc' => __( 'This will delete an inbox notification by slug', 'woocommerce' ),
+ 'selector' => array(
+ 'description' => __( 'Select an inbox notification to delete:', 'woocommerce' ),
+ 'class' => 'wc-product-search',
+ 'search_action' => 'woocommerce_json_inbox_notifications_search',
+ 'name' => 'delete_inbox_notification_note_id',
+ 'placeholder' => esc_attr__( 'Search for an inbox notification…', 'woocommerce' ),
+ ),
+ 'callback' => function () {
+ check_ajax_referer( 'debug_action', '_wpnonce' );
+
+ if ( ! isset( $_GET['delete_inbox_notification_note_id'] ) ) {
+ return __( 'No inbox notification selected', 'woocommerce' );
+ }
+ $note_id = wc_clean( sanitize_text_field( wp_unslash( $_GET['delete_inbox_notification_note_id'] ) ) );
+ $note = Notes::get_note( $note_id );
+
+ if ( ! $note ) {
+ return __( 'Inbox notification not found', 'woocommerce' );
+ }
+
+ $note->delete( true );
+ return __( 'Inbox notification has been deleted', 'woocommerce' );
+ },
+ );
+
+ return $tools;
+ }
+
+ /**
+ * Add ajax action for remote inbox notification search.
+ *
+ * @return void
+ */
+ private static function ajax_action_inbox_notification_search() {
+ global $wpdb;
+
+ check_ajax_referer( 'search-products', 'security' );
+
+ if ( ! isset( $_GET['term'] ) ) {
+ wp_send_json( array() );
+ }
+
+ $search = wc_clean( sanitize_text_field( wp_unslash( $_GET['term'] ) ) );
+ $results = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT note_id, name FROM {$wpdb->prefix}wc_admin_notes WHERE name LIKE %s",
+ '%' . $wpdb->esc_like( $search ) . '%'
+ )
+ );
+ $rows = array();
+ foreach ( $results as $result ) {
+ $rows[ $result->note_id ] = $result->name;
+ }
+ wp_send_json( $rows );
+ }
}
diff --git a/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/EvaluationLogger.php b/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/EvaluationLogger.php
index e2cf71eee5d..89010ca6960 100644
--- a/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/EvaluationLogger.php
+++ b/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/EvaluationLogger.php
@@ -25,23 +25,23 @@ class EvaluationLogger {
/**
* Logger class to use.
*
- * @var WC_Logger_Interface|null
+ * @var \WC_Logger_Interface|null
*/
private $logger;
/**
* Logger source.
*
- * @var string logger source.
+ * @var string Logger source.
*/
private $source = '';
/**
* EvaluationLogger constructor.
*
- * @param string $slug Slug of a spec that is being evaluated.
- * @param null $source Logger source.
- * @param \WC_Logger_Interface $logger Logger class to use.
+ * @param string $slug Slug/ID of a spec that is being evaluated.
+ * @param string|null $source Logger source.
+ * @param \WC_Logger_Interface|null $logger Logger class to use. Default to using the WC logger.
*/
public function __construct( $slug, $source = null, \WC_Logger_Interface $logger = null ) {
$this->slug = $slug;
@@ -59,16 +59,13 @@ class EvaluationLogger {
/**
* Add evaluation result of a rule.
*
- * @param string $rule_type name of the rule being tested.
- * @param boolean $result result of a given rule.
+ * @param string $rule_type Name of the rule being tested.
+ * @param boolean $result Result of a given rule.
*/
public function add_result( $rule_type, $result ) {
- array_push(
- $this->results,
- array(
- 'rule' => $rule_type,
- 'result' => $result ? 'passed' : 'failed',
- )
+ $this->results[] = array(
+ 'rule' => $rule_type,
+ 'result' => $result ? 'passed' : 'failed',
);
}
@@ -76,7 +73,16 @@ class EvaluationLogger {
* Log the results.
*/
public function log() {
- if ( false === defined( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' ) || true !== constant( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' ) ) {
+ $should_log = defined( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' ) && true === constant( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' );
+
+ /**
+ * Filter to determine if the rule evaluator should log the results.
+ *
+ * @since 9.2.0
+ *
+ * @param bool $should_log Whether the rule evaluator should log the results.
+ */
+ if ( ! apply_filters( 'woocommerce_admin_remote_specs_evaluator_should_log', $should_log ) ) {
return;
}
diff --git a/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/RuleEvaluator.php b/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/RuleEvaluator.php
index a4f3977d9b8..0604305c368 100644
--- a/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/RuleEvaluator.php
+++ b/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/RuleEvaluator.php
@@ -36,9 +36,9 @@ class RuleEvaluator {
* Evaluate the given rules as an AND operation - return false early if a
* rule evaluates to false.
*
- * @param array|object $rules The rule or rules being processed.
+ * @param array|object $rules The rule or rules being processed.
* @param object|null $stored_state Stored state.
- * @param array $logger_args Arguments for the event logger. `slug` is required.
+ * @param array $logger_args Arguments for the rule evaluator logger. `slug` is required.
*
* @throws \InvalidArgumentException Thrown when $logger_args is missing slug.
*
@@ -65,13 +65,16 @@ class RuleEvaluator {
throw new \InvalidArgumentException( 'Missing required field: slug in $logger_args.' );
}
- array_key_exists( 'source', $logger_args ) ? $source = $logger_args['source'] : $source = null;
+ $source = isset( $logger_args['source'] ) ? $logger_args['source'] : null;
$evaluation_logger = new EvaluationLogger( $logger_args['slug'], $source );
}
foreach ( $rules as $rule ) {
if ( ! is_object( $rule ) ) {
+ $evaluation_logger && $evaluation_logger->add_result( 'rule not an object', false );
+ $evaluation_logger && $evaluation_logger->log();
+
return false;
}
diff --git a/plugins/woocommerce/src/Blocks/AIContent/PatternsDictionary.php b/plugins/woocommerce/src/Blocks/AIContent/PatternsDictionary.php
new file mode 100644
index 00000000000..cb87c48b8ea
--- /dev/null
+++ b/plugins/woocommerce/src/Blocks/AIContent/PatternsDictionary.php
@@ -0,0 +1,673 @@
+ 'Banner',
+ 'slug' => 'woocommerce-blocks/banner',
+ 'images_total' => 1,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Up to 60% off', 'woocommerce' ),
+ 'ai_prompt' => __( 'A four words title advertising the sale', 'woocommerce' ),
+ ],
+ ],
+ 'descriptions' => [
+ [
+ 'default' => __( 'Holiday Sale', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words label with the sale name', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Get your favorite vinyl at record-breaking prices.', 'woocommerce' ),
+ 'ai_prompt' => __( 'The main description of the sale with at least 65 characters', 'woocommerce' ),
+ ],
+ ],
+ 'buttons' => [
+ [
+ 'default' => __( 'Shop vinyl records', 'woocommerce' ),
+ 'ai_prompt' => __( 'A 3 words button text to go to the sale page', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Discount Banner',
+ 'slug' => 'woocommerce-blocks/discount-banner',
+ 'content' => [
+ 'descriptions' => [
+ [
+ 'default' => __( 'Select products', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words description of the products on sale', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Discount Banner with Image',
+ 'slug' => 'woocommerce-blocks/discount-banner-with-image',
+ 'images_total' => 1,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'descriptions' => [
+ [
+ 'default' => __( 'Select products', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words description of the products on sale', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Featured Category Focus',
+ 'slug' => 'woocommerce-blocks/featured-category-focus',
+ 'images_total' => 1,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Black and white high-quality prints', 'woocommerce' ),
+ 'ai_prompt' => __( 'The four words title of the featured category related to the following image description: [image.0]', 'woocommerce' ),
+ ],
+ ],
+ 'buttons' => [
+ [
+ 'default' => __( 'Shop prints', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words button text to go to the featured category', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Featured Category Triple',
+ 'slug' => 'woocommerce-blocks/featured-category-triple',
+ 'images_total' => 3,
+ 'images_format' => 'portrait',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Home decor', 'woocommerce' ),
+ 'ai_prompt' => __( 'A one-word graphic title that encapsulates the essence of the business, inspired by the following image description: [image.0] and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Retro photography', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: [image.1] and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Handmade gifts', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: [image.2] and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Featured Products: Fresh & Tasty',
+ 'slug' => 'woocommerce-blocks/featured-products-fresh-and-tasty',
+ 'images_total' => 4,
+ 'images_format' => 'portrait',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Fresh & tasty goods', 'woocommerce' ),
+ 'ai_prompt' => __( 'The title of the featured products with at least 20 characters', 'woocommerce' ),
+ ],
+ ],
+ 'descriptions' => [
+ [
+ 'default' => __( 'Sweet Organic Lemons', 'woocommerce' ),
+ 'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.0]', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Fresh Organic Tomatoes', 'woocommerce' ),
+ 'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.1]', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Fresh Lettuce (Washed)', 'woocommerce' ),
+ 'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.2]', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Russet Organic Potatoes', 'woocommerce' ),
+ 'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.3]', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Hero Product 3 Split',
+ 'slug' => 'woocommerce-blocks/hero-product-3-split',
+ 'images_total' => 1,
+ 'images_format' => 'portrait',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Timeless elegance', 'woocommerce' ),
+ 'ai_prompt' => __( 'Write a two words title for advertising the store', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Durable glass', 'woocommerce' ),
+ 'ai_prompt' => __( 'Write a two words title for advertising the store', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Versatile charm', 'woocommerce' ),
+ 'ai_prompt' => __( 'Write a two words title for advertising the store', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'New: Retro Glass Jug', 'woocommerce' ),
+ 'ai_prompt' => __( 'Write a title with less than 20 characters for advertising the store', 'woocommerce' ),
+ ],
+ ],
+ 'descriptions' => [
+ [
+ 'default' => __( 'Elevate your table with a 330ml Retro Glass Jug, blending classic design and durable hardened glass.', 'woocommerce' ),
+ 'ai_prompt' => __( 'Write a text with approximately 130 characters, to describe a product the business is selling', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Crafted from resilient thick glass, this jug ensures lasting quality, making it perfect for everyday use with a touch of vintage charm.', 'woocommerce' ),
+ 'ai_prompt' => __( 'Write a text with approximately 130 characters, to describe a product the business is selling', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( "The Retro Glass Jug's classic silhouette effortlessly complements any setting, making it the ideal choice for serving beverages with style and flair.", 'woocommerce' ),
+ 'ai_prompt' => __( 'Write a long text, with at least 130 characters, to describe a product the business is selling', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Hero Product Chessboard',
+ 'slug' => 'woocommerce-blocks/hero-product-chessboard',
+ 'images_total' => 2,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Quality Materials', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words title describing the first displayed product feature', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Unique design', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words title describing the second displayed product feature', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Make your house feel like home', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words title describing the fourth displayed product feature', 'woocommerce' ),
+ ],
+ ],
+ 'descriptions' => [
+ [
+ 'default' => __( 'We use only the highest-quality materials in our products, ensuring that they look great and last for years to come.', 'woocommerce' ),
+ 'ai_prompt' => __( 'A description of the product feature with at least 115 characters', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'From bold prints to intricate details, our products are a perfect combination of style and function.', 'woocommerce' ),
+ 'ai_prompt' => __( 'A description of the product feature with at least 115 characters', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Add a touch of charm and coziness this holiday season with a wide selection of hand-picked decorations — from minimalist vases to designer furniture.', 'woocommerce' ),
+ 'ai_prompt' => __( 'A description of the product feature with at least 115 characters', 'woocommerce' ),
+ ],
+ ],
+ 'buttons' => [
+ [
+ 'default' => __( 'Shop home decor', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words button text to go to the product page', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Hero Product Split',
+ 'slug' => 'woocommerce-blocks/hero-product-split',
+ 'images_total' => 1,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Keep dry with 50% off rain jackets', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the product the store is selling with at least 35 characters', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Just Arrived Full Hero',
+ 'slug' => 'woocommerce-blocks/just-arrived-full-hero',
+ 'images_total' => 1,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Sound like no other', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 10 characters', 'woocommerce' ),
+ ],
+ ],
+ 'descriptions' => [
+ [
+ 'default' => __( 'Experience your music like never before with our latest generation of hi-fidelity headphones.', 'woocommerce' ),
+ 'ai_prompt' => __( 'A description of the product collection with at least 35 characters', 'woocommerce' ),
+ ],
+ ],
+ 'buttons' => [
+ [
+ 'default' => __( 'Shop now', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words button text to go to the product collection page', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Product Collection Banner',
+ 'slug' => 'woocommerce-blocks/product-collection-banner',
+ 'images_total' => 1,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Brand New for the Holidays', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 25 characters related to the following image description: [image.0]', 'woocommerce' ),
+ ],
+ ],
+ 'descriptions' => [
+ [
+ 'default' => __( 'Check out our brand new collection of holiday products and find the right gift for anyone.', 'woocommerce' ),
+ 'ai_prompt' => __( 'A description of the product collection with at least 90 characters', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Product Collections Featured Collection',
+ 'slug' => 'woocommerce-blocks/product-collections-featured-collection',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => "This week's popular products",
+ 'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 30 characters', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Product Collections Featured Collections',
+ 'slug' => 'woocommerce-blocks/product-collections-featured-collections',
+ 'images_total' => 4,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Tech gifts under $100', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the product collection with at least 20 characters related to the following image descriptions: [image.0], [image.1]', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'For the gamers', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the product collection with at least 15 characters related to the following image descriptions: [image.2], [image.3]', 'woocommerce' ),
+ ],
+ ],
+ 'buttons' => [
+ [
+ 'default' => __( 'Shop tech', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words button text to go to the product collection page', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Shop games', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words button text to go to the product collection page', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Product Collections Newest Arrivals',
+ 'slug' => 'woocommerce-blocks/product-collections-newest-arrivals',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Our newest arrivals', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 20 characters', 'woocommerce' ),
+ ],
+ ],
+ 'buttons' => [
+ [
+ 'default' => __( 'More new products', 'woocommerce' ),
+ 'ai_prompt' => __( 'The button text to go to the product collection page with at least 15 characters', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Product Collection 4 Columns',
+ 'slug' => 'woocommerce-blocks/product-collection-4-columns',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Staff picks', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 20 characters', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Product Collection 5 Columns',
+ 'slug' => 'woocommerce-blocks/product-collection-5-columns',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Our latest and greatest', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase with that advertises the product collection with at least 20 characters', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Product Gallery',
+ 'slug' => 'woocommerce-blocks/product-query-product-gallery',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Bestsellers', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the featured products with at least 10 characters', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Featured Products 2 Columns',
+ 'slug' => 'woocommerce-blocks/featured-products-2-cols',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Fan favorites', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the featured products with at least 10 characters', 'woocommerce' ),
+ ],
+ ],
+ 'descriptions' => [
+ [
+ 'default' => __( 'Get ready to start the season right. All the fan favorites in one place at the best price.', 'woocommerce' ),
+ 'ai_prompt' => __( 'A description of the featured products with at least 90 characters', 'woocommerce' ),
+ ],
+ ],
+ 'buttons' => [
+ [
+ 'default' => __( 'Shop All', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words button text to go to the featured products page', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Product Hero 2 Column 2 Row',
+ 'slug' => 'woocommerce-blocks/product-hero-2-col-2-row',
+ 'images_total' => 2,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'The Eden Jacket', 'woocommerce' ),
+ 'ai_prompt' => __( 'A three words title that advertises a product related to the following image description: [image.0]', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( '100% Woolen', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words title that advertises a product feature', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Fits your wardrobe', 'woocommerce' ),
+ 'ai_prompt' => __( 'A three words title that advertises a product feature', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Versatile', 'woocommerce' ),
+ 'ai_prompt' => __( 'An one word title that advertises a product feature', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Normal Fit', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words title that advertises a product feature', 'woocommerce' ),
+ ],
+ ],
+ 'descriptions' => [
+ [
+ 'default' => __( 'Perfect for any look featuring a mid-rise, relax fitting silhouette.', 'woocommerce' ),
+ 'ai_prompt' => __( 'The description of a product with at least 65 characters related to the following image: [image.0]', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Reflect your fashionable style.', 'woocommerce' ),
+ 'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Half tuck into your pants or layer over.', 'woocommerce' ),
+ 'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Button-down front for any type of mood or look.', 'woocommerce' ),
+ 'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( '42% Cupro 34% Linen 24% Viscose', 'woocommerce' ),
+ 'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ),
+ ],
+ ],
+ 'buttons' => [
+ [
+ 'default' => __( 'View product', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words button text to go to the product page', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Shop by Price',
+ 'slug' => 'woocommerce-blocks/shop-by-price',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Outdoor Furniture & Accessories', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the first product collection with at least 30 characters', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Summer Dinning', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the second product collection with at least 20 characters', 'woocommerce' ),
+ ],
+ [
+ 'default' => "Women's Styles",
+ 'ai_prompt' => __( 'An impact phrase that advertises the third product collection with at least 20 characters', 'woocommerce' ),
+ ],
+ [
+ 'default' => "Kids' Styles",
+ 'ai_prompt' => __( 'An impact phrase that advertises the fourth product collection with at least 20 characters', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Small Discount Banner with Image',
+ 'slug' => 'woocommerce-blocks/small-discount-banner-with-image',
+ 'images_total' => 1,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Chairs', 'woocommerce' ),
+ 'ai_prompt' => __( 'A single word that advertises the product and is related to the following image description: [image.0]', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Social: Follow us on social media',
+ 'slug' => 'woocommerce-blocks/social-follow-us-in-social-media',
+ 'images_total' => 4,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Stay in the loop', 'woocommerce' ),
+ 'ai_prompt' => __( 'A phrase that advertises the social media accounts of the store with at least 25 characters', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Alternating Image and Text',
+ 'slug' => 'woocommerce-blocks/alt-image-and-text',
+ 'images_total' => 2,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Our products', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words impact phrase that advertises the products', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Sustainable blends, stylish accessories', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the products with at least 40 characters and related to the following image description: [image.0]', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'About us', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words impact phrase that advertises the brand', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Committed to a greener lifestyle', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the brand with at least 50 characters related to the following image description: [image.1]', 'woocommerce' ),
+ ],
+ ],
+ 'descriptions' => [
+ [
+ 'default' => __( 'Indulge in the finest organic coffee beans, teas, and hand-picked accessories, all locally sourced and sustainable for a mindful lifestyle.', 'woocommerce' ),
+ 'ai_prompt' => __( 'A description of the products with at least 180 characters', 'woocommerce' ),
+ ],
+ [
+ 'default' => "Our passion is crafting mindful moments with locally sourced, organic, and sustainable products. We're more than a store; we're your path to a community-driven, eco-friendly lifestyle that embraces premium quality.",
+ 'ai_prompt' => __( 'A description of the products with at least 180 characters', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Locally sourced ingredients', 'woocommerce' ),
+ 'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Premium organic blends', 'woocommerce' ),
+ 'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Hand-picked accessories', 'woocommerce' ),
+ 'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Sustainable business practices', 'woocommerce' ),
+ 'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ),
+ ],
+ ],
+ 'buttons' => [
+ [
+ 'default' => __( 'Meet us', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words button text to go to the product page', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Testimonials 3 Columns',
+ 'slug' => 'woocommerce-blocks/testimonials-3-columns',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Eclectic finds, ethical delights', 'woocommerce' ),
+ 'ai_prompt' => __( 'Write a short title advertising a testimonial from a customer', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'Sip, Shop, Savor', 'woocommerce' ),
+ 'ai_prompt' => __( 'Write a short title advertising a testimonial from a customer', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'LOCAL LOVE', 'woocommerce' ),
+ 'ai_prompt' => __( 'Write a short title advertising a testimonial from a customer', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'What our customers say', 'woocommerce' ),
+ 'ai_prompt' => __( 'Write just 4 words to advertise testimonials from customers', 'woocommerce' ),
+ ],
+ ],
+ 'descriptions' => [
+ [
+ 'default' => __( 'Transformed my daily routine with unique, eco-friendly treasures. Exceptional quality and service. Proud to support a store that aligns with my values.', 'woocommerce' ),
+ 'ai_prompt' => __( 'Write the testimonial from a customer with approximately 150 characters', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'The organic coffee beans are a revelation. Each sip feels like a journey. Beautifully crafted accessories add a touch of elegance to my home.', 'woocommerce' ),
+ 'ai_prompt' => __( 'Write the testimonial from a customer with approximately 150 characters', 'woocommerce' ),
+ ],
+ [
+ 'default' => __( 'From sustainably sourced teas to chic vases, this store is a treasure trove. Love knowing my purchases contribute to a greener planet.', 'woocommerce' ),
+ 'ai_prompt' => __( 'Write the testimonial from a customer with approximately 150 characters', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Testimonials Single',
+ 'slug' => 'woocommerce-blocks/testimonials-single',
+ 'images_total' => 1,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'A ‘brewtiful’ experience :-)', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words title that advertises the testimonial', 'woocommerce' ),
+ ],
+ ],
+ 'descriptions' => [
+ [
+ 'default' => __( 'Exceptional flavors, sustainable choices. The carefully curated collection of coffee pots and accessories turned my kitchen into a haven of style and taste.', 'woocommerce' ),
+ 'ai_prompt' => __( 'A description of the testimonial with at least 225 characters', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Featured Category Cover Image',
+ 'slug' => 'woocommerce-blocks/featured-category-cover-image',
+ 'images_total' => 1,
+ 'images_format' => 'landscape',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Sit back and relax', 'woocommerce' ),
+ 'ai_prompt' => __( 'A description for a product with at least 20 characters', 'woocommerce' ),
+ ],
+ ],
+ 'descriptions' => [
+ [
+ 'default' => __( 'With a wide range of designer chairs to elevate your living space.', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the products with at least 55 characters', 'woocommerce' ),
+ ],
+ ],
+ 'buttons' => [
+ [
+ 'default' => __( 'Shop chairs', 'woocommerce' ),
+ 'ai_prompt' => __( 'A two words button text to go to the shop page', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ [
+ 'name' => 'Product Collection: Featured Products 5 Columns',
+ 'slug' => 'woocommerce-blocks/product-collection-featured-products-5-columns',
+ 'content' => [
+ 'titles' => [
+ [
+ 'default' => __( 'Shop new arrivals', 'woocommerce' ),
+ 'ai_prompt' => __( 'An impact phrase that advertises the newest additions to the store with at least 20 characters', 'woocommerce' ),
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
+}
diff --git a/plugins/woocommerce/src/Blocks/AIContent/PatternsHelper.php b/plugins/woocommerce/src/Blocks/AIContent/PatternsHelper.php
index 26d8fb28b44..d6eceadb97e 100644
--- a/plugins/woocommerce/src/Blocks/AIContent/PatternsHelper.php
+++ b/plugins/woocommerce/src/Blocks/AIContent/PatternsHelper.php
@@ -63,7 +63,7 @@ class PatternsHelper {
/**
* Upsert the patterns AI data.
*
- * @param array $patterns_dictionary The patterns dictionary.
+ * @param array $patterns_dictionary The patterns' dictionary.
*
* @return WP_Error|null
*/
@@ -92,18 +92,12 @@ class PatternsHelper {
* @return array|WP_Error Returns pattern dictionary or WP_Error on failure.
*/
public static function get_patterns_dictionary( $pattern_slug = null ) {
- $patterns_dictionary_file = plugin_dir_path( __FILE__ ) . 'dictionary.json';
+ $default_patterns_dictionary = PatternsDictionary::get();
- if ( ! file_exists( $patterns_dictionary_file ) ) {
+ if ( empty( $default_patterns_dictionary ) ) {
return new WP_Error( 'missing_patterns_dictionary', __( 'The patterns dictionary is missing.', 'woocommerce' ) );
}
- $default_patterns_dictionary = wp_json_file_decode( $patterns_dictionary_file, array( 'associative' => true ) );
-
- if ( json_last_error() !== JSON_ERROR_NONE ) {
- return new WP_Error( 'json_decode_error', __( 'Error decoding JSON.', 'woocommerce' ) );
- }
-
$patterns_dictionary = '';
$ai_connection_allowed = get_option( 'woocommerce_blocks_allow_ai_connection' );
diff --git a/plugins/woocommerce/src/Blocks/AIContent/UpdatePatterns.php b/plugins/woocommerce/src/Blocks/AIContent/UpdatePatterns.php
index bc0c90b0b8a..d9e792e85eb 100644
--- a/plugins/woocommerce/src/Blocks/AIContent/UpdatePatterns.php
+++ b/plugins/woocommerce/src/Blocks/AIContent/UpdatePatterns.php
@@ -402,13 +402,13 @@ class UpdatePatterns {
* @return mixed|WP_Error|null
*/
public static function get_patterns_dictionary() {
- $patterns_dictionary = plugin_dir_path( __FILE__ ) . 'dictionary.json';
+ $patterns_dictionary = PatternsDictionary::get();
- if ( ! file_exists( $patterns_dictionary ) ) {
+ if ( empty( $patterns_dictionary ) ) {
return new WP_Error( 'missing_patterns_dictionary', __( 'The patterns dictionary is missing.', 'woocommerce' ) );
}
- return wp_json_file_decode( $patterns_dictionary, array( 'associative' => true ) );
+ return $patterns_dictionary;
}
/**
diff --git a/plugins/woocommerce/src/Blocks/AIContent/UpdateProducts.php b/plugins/woocommerce/src/Blocks/AIContent/UpdateProducts.php
index 0a00b68051f..20e0549f8e3 100644
--- a/plugins/woocommerce/src/Blocks/AIContent/UpdateProducts.php
+++ b/plugins/woocommerce/src/Blocks/AIContent/UpdateProducts.php
@@ -26,7 +26,7 @@ class UpdateProducts {
'price' => 249,
],
[
- 'title' => 'Black and White Summer Portrait',
+ 'title' => 'Black and White',
'image' => 'assets/images/pattern-placeholders/white-black-black-and-white-photograph-monochrome-photography.jpg',
'description' => 'This 24" x 30" high-quality print just exudes summer. Hang it on the wall and forget about the world outside.',
'price' => 115,
@@ -113,7 +113,7 @@ class UpdateProducts {
$products_to_create = max( 0, 6 - $real_products_count - $dummy_products_count );
while ( $products_to_create > 0 ) {
$this->create_new_product( self::DUMMY_PRODUCTS[ $products_to_create - 1 ] );
- $products_to_create--;
+ --$products_to_create;
}
// Identify dummy products that need to have their content updated.
@@ -327,7 +327,7 @@ class UpdateProducts {
public function assign_ai_selected_images_to_dummy_products( $dummy_products_to_update, $ai_selected_images ) {
$products_information_list = [];
$dummy_products_count = count( $dummy_products_to_update );
- for ( $i = 0; $i < $dummy_products_count; $i ++ ) {
+ for ( $i = 0; $i < $dummy_products_count; $i++ ) {
$image_src = $ai_selected_images[ $i ]['URL'] ?? '';
if ( wc_is_valid_url( $image_src ) ) {
@@ -396,7 +396,7 @@ class UpdateProducts {
$ai_request_retries = 0;
$success = false;
while ( $ai_request_retries < 5 && ! $success ) {
- $ai_request_retries ++;
+ ++$ai_request_retries;
$ai_response = $ai_connection->fetch_ai_response( $token, $formatted_prompt, 30 );
if ( is_wp_error( $ai_response ) ) {
continue;
@@ -464,7 +464,7 @@ class UpdateProducts {
$this->product_update( $product, $product_image_id, self::DUMMY_PRODUCTS[ $i ]['title'], self::DUMMY_PRODUCTS[ $i ]['description'], self::DUMMY_PRODUCTS[ $i ]['price'] );
- $i++;
+ ++$i;
}
}
diff --git a/plugins/woocommerce/src/Blocks/AIContent/dictionary.json b/plugins/woocommerce/src/Blocks/AIContent/dictionary.json
deleted file mode 100644
index 452f8aa2fb6..00000000000
--- a/plugins/woocommerce/src/Blocks/AIContent/dictionary.json
+++ /dev/null
@@ -1,656 +0,0 @@
-[
- {
- "name": "Banner",
- "slug": "woocommerce-blocks/banner",
- "images_total": 1,
- "images_format": "landscape",
- "content": {
- "titles": [
- {
- "default": "Up to 60% off",
- "ai_prompt": "A four words title advertising the sale"
- }
- ],
- "descriptions": [
- {
- "default": "Holiday Sale",
- "ai_prompt": "A two words label with the sale name"
- },
- {
- "default": "Get your favorite vinyl at record-breaking prices.",
- "ai_prompt": "The main description of the sale with at least 65 characters"
- }
- ],
- "buttons": [
- {
- "default": "Shop vinyl records",
- "ai_prompt": "A 3 words button text to go to the sale page"
- }
- ]
- }
- },
- {
- "name": "Discount Banner",
- "slug": "woocommerce-blocks/discount-banner",
- "content": {
- "descriptions": [
- {
- "default": "Select products",
- "ai_prompt": "A two words description of the products on sale"
- }
- ]
- }
- },
- {
- "name": "Discount Banner with Image",
- "slug": "woocommerce-blocks/discount-banner-with-image",
- "images_total": 1,
- "images_format": "landscape",
- "content": {
- "descriptions": [
- {
- "default": "Select products",
- "ai_prompt": "A two words description of the products on sale"
- }
- ]
- }
- },
- {
- "name": "Featured Category Focus",
- "slug": "woocommerce-blocks/featured-category-focus",
- "images_total": 1,
- "images_format": "landscape",
- "content": {
- "titles": [
- {
- "default": "Black and white high-quality prints",
- "ai_prompt": "The four words title of the featured category related to the following image description: {image.0}"
- }
- ],
- "buttons": [
- {
- "default": "Shop prints",
- "ai_prompt": "A two words button text to go to the featured category"
- }
- ]
- }
- },
- {
- "name": "Featured Category Triple",
- "slug": "woocommerce-blocks/featured-category-triple",
- "images_total": 3,
- "images_format": "portrait",
- "content": {
- "titles": [
- {
- "default": "Home decor",
- "ai_prompt": "A one-word graphic title that encapsulates the essence of the business, inspired by the following image description: {image.0} and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image"
- },
- {
- "default": "Retro photography",
- "ai_prompt": "A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: {image.1} and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image"
- },
- {
- "default": "Handmade gifts",
- "ai_prompt": "A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: {image.2} and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image"
- }
- ]
- }
- },
- {
- "name": "Featured Products: Fresh & Tasty",
- "slug": "woocommerce-blocks/featured-products-fresh-and-tasty",
- "images_total": 4,
- "images_format": "portrait",
- "content": {
- "titles": [
- {
- "default": "Fresh & tasty goods",
- "ai_prompt": "The title of the featured products with at least 20 characters"
- }
- ],
- "descriptions": [
- {
- "default": "Sweet Organic Lemons",
- "ai_prompt": "The three words description of the featured product related to the following image description: {image.0}"
- },
- {
- "default": "Fresh Organic Tomatoes",
- "ai_prompt": "The three words description of the featured product related to the following image description: {image.1}"
- },
- {
- "default": "Fresh Lettuce (Washed)",
- "ai_prompt": "The three words description of the featured product related to the following image description: {image.2}"
- },
- {
- "default": "Russet Organic Potatoes",
- "ai_prompt": "The three words description of the featured product related to the following image description: {image.3}"
- }
- ]
- }
- },
- {
- "name": "Hero Product 3 Split",
- "slug": "woocommerce-blocks/hero-product-3-split",
- "images_total": 1,
- "images_format": "portrait",
- "content": {
- "titles": [
- {
- "default": "Timeless elegance",
- "ai_prompt": "Write a two words title for advertising the store"
- },
- {
- "default": "Durable glass",
- "ai_prompt": "Write a two words title for advertising the store"
- },
- {
- "default": "Versatile charm",
- "ai_prompt": "Write a two words title for advertising the store"
- },
- {
- "default": "New: Retro Glass Jug",
- "ai_prompt": "Write a title with less than 20 characters for advertising the store"
- }
- ],
- "descriptions": [
- {
- "default": "Elevate your table with a 330ml Retro Glass Jug, blending classic design and durable hardened glass.",
- "ai_prompt": "Write a text with approximately 130 characters, to describe a product the business is selling"
- },
- {
- "default": "Crafted from resilient thick glass, this jug ensures lasting quality, making it perfect for everyday use with a touch of vintage charm.",
- "ai_prompt": "Write a text with approximately 130 characters, to describe a product the business is selling"
- },
- {
- "default": "The Retro Glass Jug's classic silhouette effortlessly complements any setting, making it the ideal choice for serving beverages with style and flair.",
- "ai_prompt": "Write a long text, with at least 130 characters, to describe a product the business is selling"
- }
- ]
- }
- },
- {
- "name": "Hero Product Chessboard",
- "slug": "woocommerce-blocks/hero-product-chessboard",
- "images_total": 2,
- "images_format": "landscape",
- "content": {
- "titles": [
- {
- "default": "Quality Materials",
- "ai_prompt": "A two words title describing the first displayed product feature"
- },
- {
- "default": "Unique design",
- "ai_prompt": "A two words title describing the second displayed product feature"
- },
- {
- "default": "Make your house feel like home",
- "ai_prompt": "A two words title describing the fourth displayed product feature"
- }
- ],
- "descriptions": [
- {
- "default": "We use only the highest-quality materials in our products, ensuring that they look great and last for years to come.",
- "ai_prompt": "A description of the product feature with at least 115 characters"
- },
- {
- "default": "From bold prints to intricate details, our products are a perfect combination of style and function.",
- "ai_prompt": "A description of the product feature with at least 115 characters"
- },
- {
- "default": "Add a touch of charm and coziness this holiday season with a wide selection of hand-picked decorations — from minimalist vases to designer furniture.",
- "ai_prompt": "A description of the product feature with at least 115 characters"
- }
- ],
- "buttons": [
- {
- "default": "Shop home decor",
- "ai_prompt": "A two words button text to go to the product page"
- }
- ]
- }
- },
- {
- "name": "Hero Product Split",
- "slug": "woocommerce-blocks/hero-product-split",
- "images_total": 1,
- "images_format": "landscape",
- "content": {
- "titles": [
- {
- "default": "Keep dry with 50% off rain jackets",
- "ai_prompt": "An impact phrase that advertises the product the store is selling with at least 35 characters"
- }
- ]
- }
- },
- {
- "name": "Just Arrived Full Hero",
- "slug": "woocommerce-blocks/just-arrived-full-hero",
- "images_total": 1,
- "images_format": "landscape",
- "content": {
- "titles": [
- {
- "default": "Sound like no other",
- "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 10 characters"
- }
- ],
- "descriptions": [
- {
- "default": "Experience your music like never before with our latest generation of hi-fidelity headphones.",
- "ai_prompt": "A description of the product collection with at least 35 characters"
- }
- ],
- "buttons": [
- {
- "default": "Shop now",
- "ai_prompt": "A two words button text to go to the product collection page"
- }
- ]
- }
- },
- {
- "name": "Product Collection Banner",
- "slug": "woocommerce-blocks/product-collection-banner",
- "images_total": 1,
- "images_format": "landscape",
- "content": {
- "titles": [
- {
- "default": "Brand New for the Holidays",
- "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 25 characters related to the following image description: {image.0}"
- }
- ],
- "descriptions": [
- {
- "default": "Check out our brand new collection of holiday products and find the right gift for anyone.",
- "ai_prompt": "A description of the product collection with at least 90 characters"
- }
- ]
- }
- },
- {
- "name": "Product Collections Featured Collection",
- "slug": "woocommerce-blocks/product-collections-featured-collection",
- "content": {
- "titles": [
- {
- "default": "This week's popular products",
- "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 30 characters"
- }
- ]
- }
- },
- {
- "name": "Product Collections Featured Collections",
- "slug": "woocommerce-blocks/product-collections-featured-collections",
- "images_total": 4,
- "images_format": "landscape",
- "content": {
- "titles": [
- {
- "default": "Tech gifts under $100",
- "ai_prompt": "An impact phrase that advertises the product collection with at least 20 characters related to the following image descriptions: {image.0}, {image.1}"
- },
- {
- "default": "For the gamers",
- "ai_prompt": "An impact phrase that advertises the product collection with at least 15 characters related to the following image descriptions: {image.2}, {image.3}"
- }
- ],
- "buttons": [
- {
- "default": "Shop tech",
- "ai_prompt": "A two words button text to go to the product collection page"
- },
- {
- "default": "Shop games",
- "ai_prompt": "A two words button text to go to the product collection page"
- }
- ]
- }
- },
- {
- "name": "Product Collections Newest Arrivals",
- "slug": "woocommerce-blocks/product-collections-newest-arrivals",
- "content": {
- "titles": [
- {
- "default": "Our newest arrivals",
- "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 20 characters"
- }
- ],
- "buttons": [
- {
- "default": "More new products",
- "ai_prompt": "The button text to go to the product collection page with at least 15 characters"
- }
- ]
- }
- },
- {
- "name": "Product Collection 4 Columns",
- "slug": "woocommerce-blocks/product-collection-4-columns",
- "content": {
- "titles": [
- {
- "default": "Staff picks",
- "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 20 characters"
- }
- ]
- }
- },
- {
- "name": "Product Collection 5 Columns",
- "slug": "woocommerce-blocks/product-collection-5-columns",
- "content": {
- "titles": [
- {
- "default": "Our latest and greatest",
- "ai_prompt": "An impact phrase with that advertises the product collection with at least 20 characters"
- }
- ]
- }
- },
- {
- "name": "Product Gallery",
- "slug": "woocommerce-blocks/product-query-product-gallery",
- "content": {
- "titles": [
- {
- "default": "Bestsellers",
- "ai_prompt": "An impact phrase that advertises the featured products with at least 10 characters"
- }
- ]
- }
- },
- {
- "name": "Featured Products 2 Columns",
- "slug": "woocommerce-blocks/featured-products-2-cols",
- "content": {
- "titles": [
- {
- "default": "Fan favorites",
- "ai_prompt": "An impact phrase that advertises the featured products with at least 10 characters"
- }
- ],
- "descriptions": [
- {
- "default": "Get ready to start the season right. All the fan favorites in one place at the best price.",
- "ai_prompt": "A description of the featured products with at least 90 characters"
- }
- ],
- "buttons": [
- {
- "default": "Shop All",
- "ai_prompt": "A two words button text to go to the featured products page"
- }
- ]
- }
- },
- {
- "name": "Product Hero 2 Column 2 Row",
- "slug": "woocommerce-blocks/product-hero-2-col-2-row",
- "images_total": 2,
- "images_format": "landscape",
- "content": {
- "titles": [
- {
- "default": "The Eden Jacket",
- "ai_prompt": "A three words title that advertises a product related to the following image description: {image.0}"
- },
- {
- "default": "100% Woolen",
- "ai_prompt": "A two words title that advertises a product feature"
- },
- {
- "default": "Fits your wardrobe",
- "ai_prompt": "A three words title that advertises a product feature"
- },
- {
- "default": "Versatile",
- "ai_prompt": "An one word title that advertises a product feature"
- },
- {
- "default": "Normal Fit",
- "ai_prompt": "A two words title that advertises a product feature"
- }
- ],
- "descriptions": [
- {
- "default": "Perfect for any look featuring a mid-rise, relax fitting silhouette.",
- "ai_prompt": "The description of a product with at least 65 characters related to the following image: {image.0}"
- },
- {
- "default": "Reflect your fashionable style.",
- "ai_prompt": "The description of a product feature with at least 30 characters"
- },
- {
- "default": "Half tuck into your pants or layer over.",
- "ai_prompt": "The description of a product feature with at least 30 characters"
- },
- {
- "default": "Button-down front for any type of mood or look.",
- "ai_prompt": "The description of a product feature with at least 30 characters"
- },
- {
- "default": "42% Cupro 34% Linen 24% Viscose",
- "ai_prompt": "The description of a product feature with at least 30 characters"
- }
- ],
- "buttons": [
- {
- "default": "View product",
- "ai_prompt": "A two words button text to go to the product page"
- }
- ]
- }
- },
- {
- "name": "Shop by Price",
- "slug": "woocommerce-blocks/shop-by-price",
- "content": {
- "titles": [
- {
- "default": "Outdoor Furniture & Accessories",
- "ai_prompt": "An impact phrase that advertises the first product collection with at least 30 characters"
- },
- {
- "default": "Summer Dinning",
- "ai_prompt": "An impact phrase that advertises the second product collection with at least 20 characters"
- },
- {
- "default": "Women's Styles",
- "ai_prompt": "An impact phrase that advertises the third product collection with at least 20 characters"
- },
- {
- "default": "Kids' Styles",
- "ai_prompt": "An impact phrase that advertises the fourth product collection with at least 20 characters"
- }
- ]
- }
- },
- {
- "name": "Small Discount Banner with Image",
- "slug": "woocommerce-blocks/small-discount-banner-with-image",
- "images_total": 1,
- "images_format": "landscape",
- "content": {
- "titles": [
- {
- "default": "Chairs",
- "ai_prompt": "A single word that advertises the product and is related to the following image description: {image.0}"
- }
- ]
- }
- },
- {
- "name": "Social: Follow us on social media",
- "slug": "woocommerce-blocks/social-follow-us-in-social-media",
- "images_total": 4,
- "images_format": "landscape",
- "content": {
- "titles": [
- {
- "default": "Stay in the loop",
- "ai_prompt": "A phrase that advertises the social media accounts of the store with at least 25 characters"
- }
- ]
- }
- },
- {
- "name": "Alternating Image and Text",
- "slug": "woocommerce-blocks/alt-image-and-text",
- "images_total": 2,
- "images_format": "landscape",
- "content": {
- "titles": [
- {
- "default": "Our products",
- "ai_prompt": "A two words impact phrase that advertises the products"
- },
- {
- "default": "Sustainable blends, stylish accessories",
- "ai_prompt": "An impact phrase that advertises the products with at least 40 characters and related to the following image description: {image.0}"
- },
- {
- "default": "About us",
- "ai_prompt": "A two words impact phrase that advertises the brand"
- },
- {
- "default": "Committed to a greener lifestyle",
- "ai_prompt": "An impact phrase that advertises the brand with at least 50 characters related to the following image description: {image.1}"
- }
- ],
- "descriptions": [
- {
- "default": "Indulge in the finest organic coffee beans, teas, and hand-picked accessories, all locally sourced and sustainable for a mindful lifestyle.",
- "ai_prompt": "A description of the products with at least 180 characters"
- },
- {
- "default": "Our passion is crafting mindful moments with locally sourced, organic, and sustainable products. We're more than a store; we're your path to a community-driven, eco-friendly lifestyle that embraces premium quality.",
- "ai_prompt": "A description of the products with at least 180 characters"
- },
- {
- "default": "Locally sourced ingredients",
- "ai_prompt": "A three word description of the products"
- },
- {
- "default": "Premium organic blends",
- "ai_prompt": "A three word description of the products"
- },
- {
- "default": "Hand-picked accessories",
- "ai_prompt": "A three word description of the products"
- },
- {
- "default": "Sustainable business practices",
- "ai_prompt": "A three word description of the products"
- }
- ],
- "buttons": [
- {
- "default": "Meet us",
- "ai_prompt": "A two words button text to go to the product page"
- }
- ]
- }
- },
- {
- "name": "Testimonials 3 Columns",
- "slug": "woocommerce-blocks/testimonials-3-columns",
- "content": {
- "titles": [
- {
- "default": "Eclectic finds, ethical delights",
- "ai_prompt": "Write a short title advertising a testimonial from a customer"
- },
- {
- "default": "Sip, Shop, Savor",
- "ai_prompt": "Write a short title advertising a testimonial from a customer"
- },
- {
- "default": "LOCAL LOVE",
- "ai_prompt": "Write a short title advertising a testimonial from a customer"
- },
- {
- "default": "What our customers say",
- "ai_prompt": "Write just 4 words to advertise testimonials from customers"
- }
- ],
- "descriptions": [
- {
- "default": "Transformed my daily routine with unique, eco-friendly treasures. Exceptional quality and service. Proud to support a store that aligns with my values.",
- "ai_prompt": "Write the testimonial from a customer with approximately 150 characters"
- },
- {
- "default": "The organic coffee beans are a revelation. Each sip feels like a journey. Beautifully crafted accessories add a touch of elegance to my home.",
- "ai_prompt": "Write the testimonial from a customer with approximately 150 characters"
- },
- {
- "default": "From sustainably sourced teas to chic vases, this store is a treasure trove. Love knowing my purchases contribute to a greener planet.",
- "ai_prompt": "Write the testimonial from a customer with approximately 150 characters"
- }
- ]
- }
- },
- {
- "name": "Testimonials Single",
- "slug": "woocommerce-blocks/testimonials-single",
- "images_total": 1,
- "images_format": "landscape",
- "content": {
- "titles": [
- {
- "default": "A ‘brewtiful’ experience :-)",
- "ai_prompt": "A two words title that advertises the testimonial"
- }
- ],
- "descriptions": [
- {
- "default": "Exceptional flavors, sustainable choices. The carefully curated collection of coffee pots and accessories turned my kitchen into a haven of style and taste.",
- "ai_prompt": "A description of the testimonial with at least 225 characters"
- }
- ]
- }
- },
- {
- "name": "Featured Category Cover Image",
- "slug": "woocommerce-blocks/featured-category-cover-image",
- "images_total": 1,
- "images_format": "landscape",
- "content": {
- "titles": [
- {
- "default": "Sit back and relax",
- "ai_prompt": "A description for a product with at least 20 characters"
- }
- ],
- "descriptions": [
- {
- "default": "With a wide range of designer chairs to elevate your living space.",
- "ai_prompt": "An impact phrase that advertises the products with at least 55 characters"
- }
- ],
- "buttons": [
- {
- "default": "Shop chairs",
- "ai_prompt": "A two words button text to go to the shop page"
- }
- ]
- }
- },
- {
- "name": "Product Collection: Featured Products 5 Columns",
- "slug": "woocommerce-blocks/product-collection-featured-products-5-columns",
- "content": {
- "titles": [
- {
- "default": "Shop new arrivals",
- "ai_prompt": "An impact phrase that advertises the newest additions to the store with at least 20 characters"
- }
- ]
- }
- }
-]
diff --git a/plugins/woocommerce/src/Blocks/AssetsController.php b/plugins/woocommerce/src/Blocks/AssetsController.php
index 01cd1228a9e..1bb55c0f0c1 100644
--- a/plugins/woocommerce/src/Blocks/AssetsController.php
+++ b/plugins/woocommerce/src/Blocks/AssetsController.php
@@ -40,6 +40,7 @@ final class AssetsController {
add_action( 'admin_enqueue_scripts', array( $this, 'update_block_style_dependencies' ), 20 );
add_action( 'wp_enqueue_scripts', array( $this, 'update_block_settings_dependencies' ), 100 );
add_action( 'admin_enqueue_scripts', array( $this, 'update_block_settings_dependencies' ), 100 );
+ add_filter( 'js_do_concat', array( $this, 'skip_boost_minification_for_cart_checkout' ), 10, 2 );
}
/**
@@ -62,9 +63,14 @@ final class AssetsController {
// The price package is shared externally so has no blocks prefix.
$this->api->register_script( 'wc-price-format', 'assets/client/blocks/price-format.js', array(), false );
- $this->api->register_script( 'wc-blocks-vendors-frontend', $this->api->get_block_asset_build_path( 'wc-blocks-vendors-frontend' ), array(), false );
- $this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js', array( 'wc-blocks-vendors-frontend' ) );
- $this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js', array( 'wc-blocks-vendors-frontend' ) );
+ // Vendor scripts for blocks frontends (not including cart and checkout).
+ $this->api->register_script( 'wc-blocks-frontend-vendors', $this->api->get_block_asset_build_path( 'wc-blocks-frontend-vendors-frontend' ), array(), false );
+
+ // Cart and checkout frontend scripts.
+ $this->api->register_script( 'wc-cart-checkout-vendors', $this->api->get_block_asset_build_path( 'wc-cart-checkout-vendors-frontend' ), array(), false );
+ $this->api->register_script( 'wc-cart-checkout-base', $this->api->get_block_asset_build_path( 'wc-cart-checkout-base-frontend' ), array(), false );
+ $this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js' );
+ $this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js' );
// Register the interactivity components here for now.
$this->api->register_script( 'wc-interactivity-dropdown', 'assets/client/blocks/wc-interactivity-dropdown.js', array() );
@@ -253,6 +259,23 @@ final class AssetsController {
return $src;
}
+ /**
+ * Skip Jetpack Boost minification on older versions of Jetpack Boost where it causes issues.
+ *
+ * @param mixed $do_concat Whether to concatenate the script or not.
+ * @param mixed $handle The script handle.
+ * @return mixed
+ */
+ public function skip_boost_minification_for_cart_checkout( $do_concat, $handle ) {
+ $boost_is_outdated = defined( 'JETPACK_BOOST_VERSION' ) && version_compare( JETPACK_BOOST_VERSION, '3.4.2', '<' );
+ $scripts_to_ignore = [
+ 'wc-cart-checkout-vendors',
+ 'wc-cart-checkout-base',
+ ];
+
+ return $boost_is_outdated && in_array( $handle, $scripts_to_ignore, true ) ? false : $do_concat;
+ }
+
/**
* Add body classes to the frontend and within admin.
*
diff --git a/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php b/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php
index b9bb10582b3..966af6dfd05 100644
--- a/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php
+++ b/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php
@@ -19,6 +19,7 @@ use Automattic\WooCommerce\Blocks\Templates\ProductCategoryTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductTagTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductSearchResultsTemplate;
use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplate;
+use Automattic\WooCommerce\Blocks\Templates\ProductFiltersTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductFiltersOverlayTemplate;
/**
@@ -59,10 +60,14 @@ class BlockTemplatesRegistry {
}
if ( BlockTemplateUtils::supports_block_templates( 'wp_template_part' ) ) {
$template_parts = array(
- MiniCartTemplate::SLUG => new MiniCartTemplate(),
- CheckoutHeaderTemplate::SLUG => new CheckoutHeaderTemplate(),
- ProductFiltersOverlayTemplate::SLUG => new ProductFiltersOverlayTemplate(),
+ MiniCartTemplate::SLUG => new MiniCartTemplate(),
+ CheckoutHeaderTemplate::SLUG => new CheckoutHeaderTemplate(),
);
+
+ if ( Features::is_enabled( 'experimental-blocks' ) ) {
+ $template_parts[ ProductFiltersTemplate::SLUG ] = new ProductFiltersTemplate();
+ $template_parts[ ProductFiltersOverlayTemplate::SLUG ] = new ProductFiltersOverlayTemplate();
+ }
} else {
$template_parts = array();
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
index 78e44abfdee..60737382791 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
@@ -36,9 +36,9 @@ class Checkout extends AbstractBlock {
// This prevents the page redirecting when the cart is empty. This is so the editor still loads the page preview.
add_filter(
'woocommerce_checkout_redirect_empty_cart',
- function( $return ) {
+ function ( $redirect_empty_cart ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
- return isset( $_GET['_wp-find-template'] ) ? false : $return;
+ return isset( $_GET['_wp-find-template'] ) ? false : $redirect_empty_cart;
}
);
@@ -94,10 +94,17 @@ class Checkout extends AbstractBlock {
* @return array|string
*/
protected function get_block_type_script( $key = null ) {
+ $dependencies = [];
+
+ // Load password strength meter script asynchronously if needed.
+ if ( ! is_user_logged_in() && 'no' === get_option( 'woocommerce_registration_generate_password' ) ) {
+ $dependencies[] = 'zxcvbn-async';
+ }
+
$script = [
'handle' => 'wc-' . $this->block_name . '-block-frontend',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ),
- 'dependencies' => [],
+ 'dependencies' => $dependencies,
];
return $key ? $script[ $key ] : $script;
}
@@ -354,6 +361,7 @@ class Checkout extends AbstractBlock {
$this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ) );
$this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ) );
$this->asset_data_registry->add( 'forcedBillingAddress', 'billing_only' === get_option( 'woocommerce_ship_to_destination' ) );
+ $this->asset_data_registry->add( 'generatePassword', filter_var( get_option( 'woocommerce_registration_generate_password' ), FILTER_VALIDATE_BOOLEAN ) );
$this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled() );
$this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled() );
$this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled() );
@@ -377,7 +385,7 @@ class Checkout extends AbstractBlock {
$shipping_methods = WC()->shipping()->get_shipping_methods();
$formatted_shipping_methods = array_reduce(
$shipping_methods,
- function( $acc, $method ) {
+ function ( $acc, $method ) {
if ( in_array( $method->id, LocalPickupUtils::get_local_pickup_method_ids(), true ) ) {
return $acc;
}
@@ -405,7 +413,7 @@ class Checkout extends AbstractBlock {
$payment_methods = $this->get_enabled_payment_gateways();
$formatted_payment_methods = array_reduce(
$payment_methods,
- function( $acc, $method ) {
+ function ( $acc, $method ) {
$acc[] = [
'id' => $method->id,
'title' => $method->method_title,
@@ -427,7 +435,7 @@ class Checkout extends AbstractBlock {
$all_plugins = \get_plugins(); // Note that `get_compatible_plugins_for_feature` calls `get_plugins` internally, so this is already in cache.
$incompatible_extensions = array_reduce(
$declared_extensions['incompatible'],
- function( $acc, $item ) use ( $all_plugins ) {
+ function ( $acc, $item ) use ( $all_plugins ) {
$plugin = $all_plugins[ $item ] ?? null;
$plugin_id = $plugin['TextDomain'] ?? dirname( $item, 2 );
$plugin_name = $plugin['Name'] ?? $plugin_id;
@@ -465,7 +473,7 @@ class Checkout extends AbstractBlock {
$payment_gateways = WC()->payment_gateways->payment_gateways();
return array_filter(
$payment_gateways,
- function( $payment_gateway ) {
+ function ( $payment_gateway ) {
return 'yes' === $payment_gateway->enabled;
}
);
@@ -500,7 +508,7 @@ class Checkout extends AbstractBlock {
$payment_methods[ $payment_method_group ] = array_values(
array_filter(
$saved_payment_methods,
- function( $saved_payment_method ) use ( $payment_gateways ) {
+ function ( $saved_payment_method ) use ( $payment_gateways ) {
return in_array( $saved_payment_method['method']['gateway'], array_keys( $payment_gateways ), true );
}
)
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ComingSoon.php b/plugins/woocommerce/src/Blocks/BlockTypes/ComingSoon.php
index 6304507f161..a82be2587ee 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ComingSoon.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ComingSoon.php
@@ -21,12 +21,24 @@ class ComingSoon extends AbstractBlock {
}
/**
- * Get the frontend style handle for this block type.
+ * Enqueue frontend assets for this block, just in time for rendering.
*
- * @return null
+ * @internal This prevents the block script being enqueued on all pages. It is only enqueued as needed. Note that
+ * we intentionally do not pass 'script' to register_block_type.
+ *
+ * @param array $attributes Any attributes that currently are available from the block.
+ * @param string $content The block content.
+ * @param WP_Block $block The block object.
*/
- protected function get_block_type_style() {
- return null;
+ protected function enqueue_assets( array $attributes, $content, $block ) {
+ parent::enqueue_assets( $attributes, $content, $block );
+
+ if ( isset( $attributes['color'] ) ) {
+ wp_add_inline_style(
+ 'wc-blocks-style',
+ ':root{--woocommerce-coming-soon-color: ' . esc_html( $attributes['color'] ) . '}'
+ );
+ }
}
/**
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php b/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php
index dee420b76e7..80bd799b224 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php
@@ -34,6 +34,7 @@ class CustomerAccount extends AbstractBlock {
'anchor' => 'core/navigation',
'area' => 'header',
'callback' => 'should_unhook_block',
+ 'version' => '8.4.0',
),
);
@@ -118,19 +119,27 @@ class CustomerAccount extends AbstractBlock {
$account_link = get_option( 'woocommerce_myaccount_page_id' ) ? wc_get_account_endpoint_url( 'dashboard' ) : wp_login_url();
$allowed_svg = array(
- 'svg' => array(
+ 'svg' => array(
'class' => true,
'xmlns' => true,
'width' => true,
'height' => true,
'viewbox' => true,
),
- 'path' => array(
+ 'path' => array(
'd' => true,
'fill' => true,
'fill-rule' => true,
'clip-rule' => true,
),
+ 'circle' => array(
+ 'cx' => true,
+ 'cy' => true,
+ 'r' => true,
+ 'stroke' => true,
+ 'stroke-width' => true,
+ 'fill' => true,
+ ),
);
// Only provide aria-label if the display style is icon only.
@@ -158,14 +167,21 @@ class CustomerAccount extends AbstractBlock {
}
if ( self::DISPLAY_LINE === $attributes['iconStyle'] ) {
- return '
-
+ return '
+
+
';
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php
index e3021511405..cb5952d2d84 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php
@@ -65,6 +65,7 @@ class MiniCart extends AbstractBlock {
'position' => 'after',
'anchor' => 'core/navigation',
'area' => 'header',
+ 'version' => '8.4.0',
),
);
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php
index e2c96926fc7..6cdbbba4883 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php
@@ -154,9 +154,10 @@ class Status extends AbstractOrderConfirmationBlock {
*/
protected function render_account_notice( $order = null ) {
if ( $order && $order->get_customer_id() && 'store-api' === $order->get_created_via() ) {
- $nag = get_user_option( 'default_password_nag', $order->get_customer_id() );
+ $nag = get_user_option( 'default_password_nag', $order->get_customer_id() );
+ $generate = get_option( 'woocommerce_registration_generate_password', false );
- if ( $nag ) {
+ if ( $nag && ! $generate ) {
return wc_print_notice(
sprintf(
// translators: %s: site name.
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Totals.php b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Totals.php
index 978e26c8983..63cf4fbc354 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Totals.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Totals.php
@@ -225,7 +225,7 @@ class Totals extends AbstractOrderConfirmationBlock {
'' .
esc_html__( 'Note:', 'woocommerce' ) .
'
' .
- '' . wp_kses_post( nl2br( wptexturize( $order->get_customer_note() ) ) ) . '
' .
+ '' . wp_kses( nl2br( wptexturize( $order->get_customer_note() ) ), [] ) . '
' .
' ';
}
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php
index 77c03b2fd4c..87430430876 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php
@@ -167,8 +167,8 @@ class ProductButton extends AbstractBlock {
);
$div_directives = '
- data-wc-interactive=\'' . wp_json_encode( $interactive, JSON_NUMERIC_CHECK ) . '\'
- data-wc-context=\'' . wp_json_encode( $context, JSON_NUMERIC_CHECK ) . '\'
+ data-wc-interactive=\'' . wp_json_encode( $interactive, JSON_NUMERIC_CHECK | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) . '\'
+ data-wc-context=\'' . wp_json_encode( $context, JSON_NUMERIC_CHECK | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) . '\'
';
$button_directives = '
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
index 9d828541d02..4b9372e35f6 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php
@@ -181,7 +181,7 @@ class ProductCollection extends AbstractBlock {
'data-wc-navigation-id',
'wc-product-collection-' . $this->parsed_block['attrs']['queryId']
);
- $p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ) ) );
+ $p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
$p->set_attribute(
'data-wc-context',
wp_json_encode(
@@ -193,7 +193,7 @@ class ProductCollection extends AbstractBlock {
// This way we avoid prefetching when the page loads.
'isPrefetchNextOrPreviousLink' => false,
),
- JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP
+ JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
)
);
$block_content = $p->get_updated_html();
@@ -295,7 +295,7 @@ class ProductCollection extends AbstractBlock {
'class_name' => $class_name,
)
) ) {
- $processor->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ) ) );
+ $processor->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
$processor->set_attribute( 'data-wc-on--click', 'actions.navigate' );
$processor->set_attribute( 'data-wc-key', $key_prefix . '--' . esc_attr( wp_rand() ) );
@@ -524,7 +524,10 @@ class ProductCollection extends AbstractBlock {
// phpcs:ignore WordPress.DB.SlowDBQuery
$block_context_query['tax_query'] = ! empty( $query['tax_query'] ) ? $query['tax_query'] : array();
- $is_exclude_applied_filters = ! ( $block->context['query']['inherit'] ?? false );
+ $inherit = $block->context['query']['inherit'] ?? false;
+ $filterable = $block->context['query']['filterable'] ?? false;
+
+ $is_exclude_applied_filters = ! ( $inherit || $filterable );
return $this->get_final_frontend_query( $block_context_query, $page, $is_exclude_applied_filters );
}
@@ -1089,7 +1092,7 @@ class ProductCollection extends AbstractBlock {
$max_price_query = empty( $max_price ) ? array() : array(
'key' => '_price',
'value' => $max_price,
- 'compare' => '<',
+ 'compare' => '<=',
'type' => 'numeric',
);
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php
index 0b24b3b07d7..6cfc9a1465a 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php
@@ -115,8 +115,8 @@ final class ProductFilter extends AbstractBlock {
}
$attributes_data = array(
- 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
- 'data-wc-context' => wp_json_encode( array( 'hasSelectedFilter' => $has_selected_filter ) ),
+ 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
+ 'data-wc-context' => wp_json_encode( array( 'hasSelectedFilter' => $has_selected_filter ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'class' => 'wc-block-product-filters',
);
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php
index 722e0d0ebaf..679227f3a09 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php
@@ -55,8 +55,8 @@ final class ProductFilterActive extends AbstractBlock {
$wrapper_attributes = get_block_wrapper_attributes(
array(
- 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
- 'data-wc-context' => wp_json_encode( $context ),
+ 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
+ 'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'data-has-filter' => empty( $active_filters ) ? 'no' : 'yes',
)
);
@@ -185,7 +185,7 @@ final class ProductFilterActive extends AbstractBlock {
private function get_html_attributes( $attributes ) {
return array_reduce(
array_keys( $attributes ),
- function( $acc, $key ) use ( $attributes ) {
+ function ( $acc, $key ) use ( $attributes ) {
$acc .= sprintf( ' %1$s="%2$s"', esc_attr( $key ), esc_attr( $attributes[ $key ] ) );
return $acc;
},
@@ -227,7 +227,7 @@ final class ProductFilterActive extends AbstractBlock {
return array_filter(
$url_query_params,
- function( $key ) use ( $filter_param_keys ) {
+ function ( $key ) use ( $filter_param_keys ) {
return in_array( $key, $filter_param_keys, true );
},
ARRAY_FILTER_USE_KEY
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php
index ed4bb640c5d..352c708f8ea 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php
@@ -30,6 +30,33 @@ final class ProductFilterAttribute extends AbstractBlock {
add_filter( 'collection_filter_query_param_keys', array( $this, 'get_filter_query_param_keys' ), 10, 2 );
add_filter( 'collection_active_filters_data', array( $this, 'register_active_filters_data' ), 10, 2 );
+ add_action( 'deleted_transient', array( $this, 'delete_default_attribute_id_transient' ) );
+ }
+
+ /**
+ * Extra data passed through from server to client for block.
+ *
+ * @param array $attributes Any attributes that currently are available from the block.
+ * Note, this will be empty in the editor context when the block is
+ * not in the post content on editor load.
+ */
+ protected function enqueue_data( array $attributes = array() ) {
+ parent::enqueue_data( $attributes );
+
+ if ( is_admin() ) {
+ $this->asset_data_registry->add( 'defaultProductFilterAttribute', $this->get_default_attribute() );
+ }
+ }
+
+ /**
+ * Delete the default attribute id transient when the attribute taxonomies are deleted.
+ *
+ * @param string $transient The transient name.
+ */
+ public function delete_default_attribute_id_transient( $transient ) {
+ if ( 'wc_attribute_taxonomies' === $transient ) {
+ delete_transient( 'wc_block_product_filter_attribute_default_attribute' );
+ }
}
/**
@@ -43,7 +70,7 @@ final class ProductFilterAttribute extends AbstractBlock {
public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) {
$attribute_param_keys = array_filter(
$url_param_keys,
- function( $param ) {
+ function ( $param ) {
return strpos( $param, 'filter_' ) === 0 || strpos( $param, 'query_type_' ) === 0;
}
);
@@ -64,7 +91,7 @@ final class ProductFilterAttribute extends AbstractBlock {
public function register_active_filters_data( $data, $params ) {
$product_attributes_map = array_reduce(
wc_get_attribute_taxonomies(),
- function( $acc, $attribute_object ) {
+ function ( $acc, $attribute_object ) {
$acc[ $attribute_object->attribute_name ] = $attribute_object->attribute_label;
return $acc;
},
@@ -73,7 +100,7 @@ final class ProductFilterAttribute extends AbstractBlock {
$active_product_attributes = array_reduce(
array_keys( $params ),
- function( $acc, $attribute ) {
+ function ( $acc, $attribute ) {
if ( strpos( $attribute, 'filter_' ) === 0 ) {
$acc[] = str_replace( 'filter_', '', $attribute );
}
@@ -84,7 +111,7 @@ final class ProductFilterAttribute extends AbstractBlock {
$active_product_attributes = array_filter(
$active_product_attributes,
- function( $item ) use ( $product_attributes_map ) {
+ function ( $item ) use ( $product_attributes_map ) {
return in_array( $item, array_keys( $product_attributes_map ), true );
}
);
@@ -96,7 +123,7 @@ final class ProductFilterAttribute extends AbstractBlock {
// Get attribute term by slug.
$terms = array_map(
- function( $term ) use ( $product_attribute, $action_namespace ) {
+ function ( $term ) use ( $product_attribute, $action_namespace ) {
$term_object = get_term_by( 'slug', $term, "pa_{$product_attribute}" );
return array(
'title' => $term_object->name,
@@ -107,7 +134,8 @@ final class ProductFilterAttribute extends AbstractBlock {
'value' => $term,
'attributeSlug' => $product_attribute,
'queryType' => get_query_var( "query_type_{$product_attribute}" ),
- )
+ ),
+ JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
),
),
);
@@ -146,7 +174,7 @@ final class ProductFilterAttribute extends AbstractBlock {
'
',
get_block_wrapper_attributes(
array(
- 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
+ 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'data-has-filter' => 'no',
)
),
@@ -168,7 +196,7 @@ final class ProductFilterAttribute extends AbstractBlock {
);
$attribute_options = array_map(
- function( $term ) use ( $attribute_counts, $selected_terms ) {
+ function ( $term ) use ( $attribute_counts, $selected_terms ) {
$term = (array) $term;
$term['count'] = $attribute_counts[ $term['term_id'] ];
$term['selected'] = in_array( $term['slug'], $selected_terms, true );
@@ -179,7 +207,7 @@ final class ProductFilterAttribute extends AbstractBlock {
$filtered_options = array_filter(
$attribute_options,
- function( $option ) {
+ function ( $option ) {
return $option['count'] > 0;
}
);
@@ -191,15 +219,15 @@ final class ProductFilterAttribute extends AbstractBlock {
$context = array(
'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ),
'queryType' => $attributes['queryType'],
- 'selectType' => $attributes['selectType'],
+ 'selectType' => 'multiple',
);
return sprintf(
'
%2$s%3$s
',
get_block_wrapper_attributes(
array(
- 'data-wc-context' => wp_json_encode( $context ),
- 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
+ 'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
+ 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'data-has-filter' => 'yes',
)
),
@@ -242,7 +270,7 @@ final class ProductFilterAttribute extends AbstractBlock {
'items' => $list_items,
'action' => "{$this->get_full_block_name()}::actions.navigate",
'selected_items' => $selected_items,
- 'select_type' => $attributes['selectType'] ?? 'multiple',
+ 'select_type' => 'multiple',
// translators: %s is a product attribute name.
'placeholder' => sprintf( __( 'Select %s', 'woocommerce' ), $product_attribute->name ),
)
@@ -264,7 +292,7 @@ final class ProductFilterAttribute extends AbstractBlock {
$show_counts = $attributes['showCounts'] ?? false;
$list_options = array_map(
- function( $option ) use ( $show_counts ) {
+ function ( $option ) use ( $show_counts ) {
return array(
'id' => $option['slug'] . '-' . $option['term_id'],
'checked' => $option['selected'],
@@ -320,13 +348,72 @@ final class ProductFilterAttribute extends AbstractBlock {
$attribute_counts = array_reduce(
$attribute_counts,
- function( $acc, $count ) {
+ function ( $acc, $count ) {
$acc[ $count['term'] ] = $count['count'];
return $acc;
},
- []
+ array()
);
return $attribute_counts;
}
+
+ /**
+ * Get the attribute if with most term but closest to 30 terms.
+ *
+ * @return int
+ */
+ private function get_default_attribute() {
+ $cached = get_transient( 'wc_block_product_filter_attribute_default_attribute' );
+
+ if ( $cached ) {
+ return $cached;
+ }
+
+ $attributes = wc_get_attribute_taxonomies();
+
+ $attributes_count = array_map(
+ function ( $attribute ) {
+ return intval(
+ wp_count_terms(
+ array(
+ 'taxonomy' => 'pa_' . $attribute->attribute_name,
+ 'hide_empty' => false,
+ )
+ )
+ );
+ },
+ $attributes
+ );
+
+ asort( $attributes_count );
+
+ $search = 30;
+ $closest = null;
+ $attribute_id = null;
+
+ foreach ( $attributes_count as $id => $count ) {
+ if ( null === $closest || abs( $search - $closest ) > abs( $count - $search ) ) {
+ $closest = $count;
+ $attribute_id = $id;
+ }
+
+ if ( $closest && $count >= $search ) {
+ break;
+ }
+ }
+
+ $default_attribute = array(
+ 'id' => 0,
+ 'label' => __( 'Attribute', 'woocommerce' ),
+ );
+
+ if ( $attribute_id ) {
+ $default_attribute = $attributes[ $attribute_id ];
+ }
+
+ set_transient( 'wc_block_product_filter_attribute_default_attribute', $default_attribute );
+
+ return $default_attribute;
+ }
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPrice.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPrice.php
index 80a0ad50a7f..708868018c2 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPrice.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPrice.php
@@ -44,7 +44,7 @@ final class ProductFilterPrice extends AbstractBlock {
public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) {
$price_param_keys = array_filter(
$url_param_keys,
- function( $param ) {
+ function ( $param ) {
return self::MIN_PRICE_QUERY_VAR === $param || self::MAX_PRICE_QUERY_VAR === $param;
}
);
@@ -141,8 +141,13 @@ final class ProductFilterPrice extends AbstractBlock {
) = $attributes;
$wrapper_attributes = array(
- 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
- 'data-wc-context' => wp_json_encode( $data ),
+ 'data-wc-interactive' => wp_json_encode(
+ array(
+ 'namespace' => $this->get_full_block_name(),
+ ),
+ JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP,
+ ),
+ 'data-wc-context' => wp_json_encode( $data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'data-has-filter' => 'no',
);
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRating.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRating.php
index 8299a6028f1..76bcad3727d 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRating.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRating.php
@@ -47,7 +47,7 @@ final class ProductFilterRating extends AbstractBlock {
public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) {
$rating_param_keys = array_filter(
$url_param_keys,
- function( $param ) {
+ function ( $param ) {
return self::RATING_FILTER_QUERY_VAR === $param;
}
);
@@ -84,8 +84,8 @@ final class ProductFilterRating extends AbstractBlock {
/* translators: %d is the rating value. */
'title' => sprintf( __( 'Rated %d out of 5', 'woocommerce' ), $rating ),
'attributes' => array(
- 'data-wc-on--click' => "{$this->get_full_block_name()}::actions.removeFilter",
- 'data-wc-context' => "{$this->get_full_block_name()}::" . wp_json_encode( array( 'value' => $rating ) ),
+ 'data-wc-on--click' => esc_attr( "{$this->get_full_block_name()}::actions.removeFilter" ),
+ 'data-wc-context' => esc_attr( "{$this->get_full_block_name()}::" ) . wp_json_encode( array( 'value' => $rating ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
),
);
},
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterStockStatus.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterStockStatus.php
index 6c58b042a72..ae9295801e9 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterStockStatus.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterStockStatus.php
@@ -45,7 +45,7 @@ final class ProductFilterStockStatus extends AbstractBlock {
public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) {
$stock_param_keys = array_filter(
$url_param_keys,
- function( $param ) {
+ function ( $param ) {
return self::STOCK_STATUS_QUERY_VAR === $param;
}
);
@@ -81,12 +81,12 @@ final class ProductFilterStockStatus extends AbstractBlock {
$action_namespace = $this->get_full_block_name();
$active_stock_statuses = array_map(
- function( $status ) use ( $stock_status_options, $action_namespace ) {
+ function ( $status ) use ( $stock_status_options, $action_namespace ) {
return array(
'title' => $stock_status_options[ $status ],
'attributes' => array(
'data-wc-on--click' => "$action_namespace::actions.removeFilter",
- 'data-wc-context' => "$action_namespace::" . wp_json_encode( array( 'value' => $status ) ),
+ 'data-wc-context' => "$action_namespace::" . wp_json_encode( array( 'value' => $status ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
),
);
},
@@ -168,7 +168,7 @@ final class ProductFilterStockStatus extends AbstractBlock {
$list_items = array_values(
array_map(
- function( $item ) use ( $stock_statuses, $show_counts, $selected_stock_statuses ) {
+ function ( $item ) use ( $stock_statuses, $show_counts, $selected_stock_statuses ) {
$label = $show_counts ? $stock_statuses[ $item['status'] ] . ' (' . $item['count'] . ')' : $stock_statuses[ $item['status'] ];
return array(
'label' => $label,
@@ -183,13 +183,13 @@ final class ProductFilterStockStatus extends AbstractBlock {
$selected_items = array_values(
array_filter(
$list_items,
- function( $item ) use ( $selected_stock_statuses ) {
+ function ( $item ) use ( $selected_stock_statuses ) {
return in_array( $item['value'], $selected_stock_statuses, true );
}
)
);
- $data_directive = wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) );
+ $data_directive = wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP );
ob_start();
?>
@@ -255,7 +255,7 @@ final class ProductFilterStockStatus extends AbstractBlock {
return array_filter(
$data,
- function( $stock_count ) {
+ function ( $stock_count ) {
return $stock_count['count'] > 0;
}
);
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlay.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlay.php
index 0d14d0ef60e..449bc4f8c6c 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlay.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlay.php
@@ -1,6 +1,9 @@
%s
', esc_html__( 'Filters Overlay', 'woocommerce' ) );
- $html = ob_get_clean();
+ return $content;
+ }
- return $html;
+ /**
+ * Extra data passed through from server to client for block.
+ *
+ * @param array $attributes Any attributes that currently are available from the block.
+ * Note, this will be empty in the editor context when the block is
+ * not in the post content on editor load.
+ */
+ protected function enqueue_data( array $attributes = [] ) {
+ parent::enqueue_data( $attributes );
+
+ $template_part_edit_uri = '';
+
+ if (
+ current_user_can( 'edit_theme_options' ) &&
+ ( wc_current_theme_is_fse_theme() || current_theme_supports( 'block-template-parts' ) )
+ ) {
+ $theme_slug = BlockTemplateUtils::theme_has_template_part( 'product-filters-overlay' ) ? wp_get_theme()->get_stylesheet() : BlockTemplateUtils::PLUGIN_SLUG;
+
+ $site_editor_uri = add_query_arg(
+ array(
+ 'canvas' => 'edit',
+ 'path' => '/template-parts/single',
+ ),
+ admin_url( 'site-editor.php' )
+ );
+
+ $template_part_edit_uri = esc_url_raw(
+ add_query_arg(
+ array(
+ 'postId' => sprintf( '%s//%s', $theme_slug, 'product-filters-overlay' ),
+ 'postType' => 'wp_template_part',
+ ),
+ $site_editor_uri
+ )
+ );
+ }
+
+ $this->asset_data_registry->add(
+ 'templatePartProductFiltersOverlayEditUri',
+ $template_part_edit_uri
+ );
}
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php
index 18ccb0c8fa7..812a3baaf98 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php
@@ -59,7 +59,7 @@ class ProductGallery extends AbstractBlock {
$html = array_reduce(
$parsed_template,
- function( $carry, $item ) {
+ function ( $carry, $item ) {
return $carry . render_block( $item );
},
''
@@ -134,7 +134,7 @@ class ProductGallery extends AbstractBlock {
$p = new \WP_HTML_Tag_Processor( $html );
if ( $p->next_tag() ) {
- $p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ) ) );
+ $p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
$p->set_attribute(
'data-wc-context',
wp_json_encode(
@@ -147,7 +147,8 @@ class ProductGallery extends AbstractBlock {
'mouseIsOverPreviousOrNextButton' => false,
'productId' => $product_id,
'elementThatTriggeredDialogOpening' => null,
- )
+ ),
+ JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
)
);
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImage.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImage.php
index af79a57ae49..b7a1ea0668e 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImage.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImage.php
@@ -98,7 +98,7 @@ class ProductGalleryLargeImage extends AbstractBlock {
'{content}' => $content,
'{directives}' => array_reduce(
array_keys( $directives ),
- function( $carry, $key ) use ( $directives ) {
+ function ( $carry, $key ) use ( $directives ) {
return $carry . ' ' . $key . '="' . esc_attr( $directives[ $key ] ) . '"';
},
''
@@ -143,7 +143,7 @@ class ProductGalleryLargeImage extends AbstractBlock {
);
$main_image_with_wrapper = array_map(
- function( $main_image_element ) {
+ function ( $main_image_element ) {
return "
" . $main_image_element . ' ';
},
$main_images
@@ -151,7 +151,6 @@ class ProductGalleryLargeImage extends AbstractBlock {
$visible_main_image = array_shift( $main_images );
return array( $visible_main_image, $main_image_with_wrapper );
-
}
/**
@@ -187,8 +186,8 @@ class ProductGalleryLargeImage extends AbstractBlock {
);
return array(
- 'data-wc-interactive' => wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ) ),
- 'data-wc-context' => wp_json_encode( $context, JSON_NUMERIC_CHECK ),
+ 'data-wc-interactive' => wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
+ 'data-wc-context' => wp_json_encode( $context, JSON_NUMERIC_CHECK | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'data-wc-on--mousemove' => 'actions.startZoom',
'data-wc-on--mouseleave' => 'actions.resetZoom',
);
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImageNextPrevious.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImageNextPrevious.php
index 9b7a907321a..62421e313b4 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImageNextPrevious.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImageNextPrevious.php
@@ -129,7 +129,7 @@ class ProductGalleryLargeImageNextPrevious extends AbstractBlock {
'{next_button}' => $next_button,
'{alignment_class}' => $alignment_class,
'{position_class}' => $position_class,
- '{data_wc_interactive}' => wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ), JSON_NUMERIC_CHECK ),
+ '{data_wc_interactive}' => wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ), JSON_NUMERIC_CHECK | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
)
);
}
@@ -188,7 +188,6 @@ class ProductGalleryLargeImageNextPrevious extends AbstractBlock {
$this->get_class_suffix( $context ),
$icon_path
);
-
}
/**
@@ -229,6 +228,5 @@ class ProductGalleryLargeImageNextPrevious extends AbstractBlock {
$this->get_class_suffix( $context ),
$icon_path
);
-
}
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryPager.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryPager.php
index e3bc873cbde..58e268addc0 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryPager.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryPager.php
@@ -74,7 +74,7 @@ class ProductGalleryPager extends AbstractBlock {
',
$wrapper_attributes,
$html,
- wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ) )
+ wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP )
);
}
return '';
@@ -127,7 +127,10 @@ class ProductGalleryPager extends AbstractBlock {
$p->set_attribute(
'data-wc-context',
wp_json_encode(
- array( 'imageId' => strval( $product_gallery_image_id ) ),
+ array(
+ 'imageId' => strval( $product_gallery_image_id ),
+ ),
+ JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP,
)
);
$p->set_attribute(
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryThumbnails.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryThumbnails.php
index 09b0d8cceee..58f0765923e 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryThumbnails.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryThumbnails.php
@@ -168,7 +168,7 @@ class ProductGalleryThumbnails extends AbstractBlock {
}
}
- $thumbnails_count++;
+ ++$thumbnails_count;
}
return sprintf(
@@ -178,7 +178,7 @@ class ProductGalleryThumbnails extends AbstractBlock {
esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classes_and_styles['styles'] ),
$html,
- wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ) )
+ wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP )
);
}
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php
index 9d3f5a4ab68..ec5b78d9f6d 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php
@@ -619,7 +619,7 @@ class ProductQuery extends AbstractBlock {
$max_price_query = empty( $max_price ) ? array() : [
'key' => '_price',
'value' => $max_price,
- 'compare' => '<',
+ 'compare' => '<=',
'type' => 'numeric',
];
diff --git a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php
index 1ea78bcdc53..62453b1a5a7 100644
--- a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php
+++ b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php
@@ -386,17 +386,8 @@ class CheckoutFields {
$field_data['options'] = $cleaned_options;
- // If the field is not required, inject an empty option at the start.
- if ( isset( $field_data['required'] ) && false === $field_data['required'] && ! in_array( '', $added_values, true ) ) {
- $field_data['options'] = array_merge(
- [
- [
- 'value' => '',
- 'label' => '',
- ],
- ],
- $field_data['options']
- );
+ if ( isset( $field_data['placeholder'] ) ) {
+ $field_data['placeholder'] = sanitize_text_field( $field_data['placeholder'] );
}
return $field_data;
diff --git a/plugins/woocommerce/src/Blocks/InteractivityComponents/CheckboxList.php b/plugins/woocommerce/src/Blocks/InteractivityComponents/CheckboxList.php
index 647d44a0bb7..ed27a58af1f 100644
--- a/plugins/woocommerce/src/Blocks/InteractivityComponents/CheckboxList.php
+++ b/plugins/woocommerce/src/Blocks/InteractivityComponents/CheckboxList.php
@@ -29,12 +29,12 @@ class CheckboxList {
$checkbox_list_context = array( 'items' => $items );
$on_change = $props['on_change'] ?? '';
- $namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-checkbox-list' ) );
+ $namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-checkbox-list' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP );
ob_start();
?>
-
+
diff --git a/plugins/woocommerce/src/Blocks/InteractivityComponents/Dropdown.php b/plugins/woocommerce/src/Blocks/InteractivityComponents/Dropdown.php
index 68521bd09d0..1c6d2d13cbd 100644
--- a/plugins/woocommerce/src/Blocks/InteractivityComponents/Dropdown.php
+++ b/plugins/woocommerce/src/Blocks/InteractivityComponents/Dropdown.php
@@ -35,12 +35,12 @@ class Dropdown {
);
$action = $props['action'] ?? '';
- $namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-dropdown' ) );
+ $namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-dropdown' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP );
ob_start();
?>
-
+
@@ -118,7 +118,7 @@ class Dropdown {
data-wc-class--is-selected="state.isSelected"
class="components-form-token-field__suggestion"
data-wc-bind--aria-selected="state.isSelected"
- data-wc-context=''
+ data-wc-context=''
>
diff --git a/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php b/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php
index c5074cea0ef..2b24e1a9657 100644
--- a/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php
+++ b/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php
@@ -91,11 +91,13 @@ class PTKPatternsStore {
* @return void
*/
private function schedule_action_if_not_pending( $action ) {
- if ( as_has_scheduled_action( $action ) ) {
+ $last_request = get_transient( 'last_fetch_patterns_request' );
+ if ( as_has_scheduled_action( $action ) || false !== $last_request ) {
return;
}
as_schedule_single_action( time(), $action );
+ set_transient( 'last_fetch_patterns_request', time(), HOUR_IN_SECONDS );
}
/**
@@ -185,6 +187,7 @@ class PTKPatternsStore {
$patterns = $this->ptk_client->fetch_patterns(
array(
+ // This is the site where the patterns are stored. Despite the 'wpcomstaging.com' domain suggesting a staging environment, this URL points to the production environment where stable versions of the patterns are maintained.
'site' => 'wooblockpatterns.wpcomstaging.com',
'categories' => array(
'_woo_intro',
diff --git a/plugins/woocommerce/src/Blocks/Templates/AbstractPageTemplate.php b/plugins/woocommerce/src/Blocks/Templates/AbstractPageTemplate.php
index 2831921e3ec..a455cecb8ee 100644
--- a/plugins/woocommerce/src/Blocks/Templates/AbstractPageTemplate.php
+++ b/plugins/woocommerce/src/Blocks/Templates/AbstractPageTemplate.php
@@ -14,7 +14,6 @@ abstract class AbstractPageTemplate extends AbstractTemplate {
*/
public function init() {
add_filter( 'page_template_hierarchy', array( $this, 'page_template_hierarchy' ), 1 );
- add_filter( 'pre_get_document_title', array( $this, 'page_template_title' ) );
}
/**
@@ -48,7 +47,10 @@ abstract class AbstractPageTemplate extends AbstractTemplate {
}
/**
- * Filter the page title when the template is active.
+ * Forces the page title to match the template title when this template is active.
+ *
+ * Only applies when hooked into `pre_get_document_title`. Most templates used for pages will not require this because
+ * the page title should be used instead.
*
* @param string $title Page title.
* @return string
diff --git a/plugins/woocommerce/src/Blocks/Templates/OrderConfirmationTemplate.php b/plugins/woocommerce/src/Blocks/Templates/OrderConfirmationTemplate.php
index 29db821623e..d789aa538f2 100644
--- a/plugins/woocommerce/src/Blocks/Templates/OrderConfirmationTemplate.php
+++ b/plugins/woocommerce/src/Blocks/Templates/OrderConfirmationTemplate.php
@@ -20,7 +20,7 @@ class OrderConfirmationTemplate extends AbstractPageTemplate {
*/
public function init() {
add_action( 'wp_before_admin_bar_render', array( $this, 'remove_edit_page_link' ) );
-
+ add_filter( 'pre_get_document_title', array( $this, 'page_template_title' ) );
parent::init();
}
diff --git a/plugins/woocommerce/src/Blocks/Templates/ProductFiltersOverlayTemplate.php b/plugins/woocommerce/src/Blocks/Templates/ProductFiltersOverlayTemplate.php
index 6070ff238ef..38c19590275 100644
--- a/plugins/woocommerce/src/Blocks/Templates/ProductFiltersOverlayTemplate.php
+++ b/plugins/woocommerce/src/Blocks/Templates/ProductFiltersOverlayTemplate.php
@@ -20,14 +20,12 @@ class ProductFiltersOverlayTemplate extends AbstractTemplatePart {
*
* @var string
*/
- public $template_area = 'product-filters-overlay';
+ public $template_area = 'uncategorized';
/**
* Initialization method.
*/
- public function init() {
- add_filter( 'default_wp_template_part_areas', array( $this, 'register_product_filters_overlay_template_part_area' ), 10, 1 );
- }
+ public function init() {}
/**
* Returns the title of the template.
@@ -46,21 +44,4 @@ class ProductFiltersOverlayTemplate extends AbstractTemplatePart {
public function get_template_description() {
return __( 'Template used to display the Product Filters Overlay.', 'woocommerce' );
}
-
- /**
- * Add Filters Overlay to the default template part areas.
- *
- * @param array $default_area_definitions An array of supported area objects.
- * @return array The supported template part areas including the Filters Overlay one.
- */
- public function register_product_filters_overlay_template_part_area( $default_area_definitions ) {
- $product_filters_overlay_template_part_area = array(
- 'area' => 'product-filters-overlay',
- 'label' => $this->get_template_title(),
- 'description' => $this->get_template_description(),
- 'icon' => 'filter',
- 'area_tag' => 'product-filters-overlay',
- );
- return array_merge( $default_area_definitions, array( $product_filters_overlay_template_part_area ) );
- }
}
diff --git a/plugins/woocommerce/src/Blocks/Templates/ProductFiltersTemplate.php b/plugins/woocommerce/src/Blocks/Templates/ProductFiltersTemplate.php
new file mode 100644
index 00000000000..ece799a312f
--- /dev/null
+++ b/plugins/woocommerce/src/Blocks/Templates/ProductFiltersTemplate.php
@@ -0,0 +1,95 @@
+name ) {
+ return $variations;
+ }
+
+ // If template part is modified, Core will pick it up and register a variation
+ // for it. Check if the variation already exists before adding it.
+ foreach ( $variations as $variation ) {
+ if ( ! empty( $variation['attributes']['slug'] ) && 'product-filters' === $variation['attributes']['slug'] ) {
+ return $variations;
+ }
+ }
+
+ $theme = 'woocommerce/woocommerce';
+ // Check if current theme overrides this template part.
+ if ( BlockTemplateUtils::theme_has_template_part( 'product-filters' ) ) {
+ $theme = wp_get_theme()->get( 'TextDomain' );
+ }
+
+ $variations[] = array(
+ 'name' => 'file_' . self::SLUG,
+ 'title' => $this->get_template_title(),
+ 'description' => true,
+ 'attributes' => array(
+ 'slug' => self::SLUG,
+ 'theme' => $theme,
+ 'area' => $this->template_area,
+ ),
+ 'scope' => array( 'inserter' ),
+ 'icon' => 'layout',
+ );
+ return $variations;
+ }
+}
diff --git a/plugins/woocommerce/src/Blocks/Utils/BlockHooksTrait.php b/plugins/woocommerce/src/Blocks/Utils/BlockHooksTrait.php
index 4ac4282378c..2bbc2a89637 100644
--- a/plugins/woocommerce/src/Blocks/Utils/BlockHooksTrait.php
+++ b/plugins/woocommerce/src/Blocks/Utils/BlockHooksTrait.php
@@ -18,31 +18,34 @@ trait BlockHooksTrait {
* @return array An array of block slugs hooked into a given context.
*/
public function register_hooked_block( $hooked_blocks, $position, $anchor_block, $context ) {
-
- /**
- * If the block has no hook placements, return early.
- */
+ // If the block has no hook placements, return early.
if ( ! isset( $this->hooked_block_placements ) || empty( $this->hooked_block_placements ) ) {
return $hooked_blocks;
}
- // Cache for active theme.
- static $active_theme_name = null;
- if ( is_null( $active_theme_name ) ) {
- $active_theme_name = wp_get_theme()->get( 'Name' );
+ // Cache the block hooks version.
+ static $block_hooks_version = null;
+ if ( defined( 'WP_RUN_CORE_TESTS' ) || is_null( $block_hooks_version ) ) {
+ $block_hooks_version = get_option( 'woocommerce_hooked_blocks_version' );
}
- /**
- * A list of theme slugs to execute this with. This is a temporary
- * measure until improvements to the Block Hooks API allow for exposing
- * to all block themes.
- *
- * @since 8.4.0
- */
- $theme_include_list = apply_filters( 'woocommerce_hooked_blocks_theme_include_list', array( 'Twenty Twenty-Four', 'Twenty Twenty-Three', 'Twenty Twenty-Two', 'Tsubaki', 'Zaino', 'Thriving Artist', 'Amulet', 'Tazza' ) );
+ // If block hooks are disabled or the version is not set, return early.
+ if ( 'no' === $block_hooks_version || false === $block_hooks_version ) {
+ return $hooked_blocks;
+ }
- if ( $context && in_array( $active_theme_name, $theme_include_list, true ) ) {
- foreach ( $this->hooked_block_placements as $placement ) {
+ // Valid placements are those that have no version specified,
+ // or have a version that is less than or equal to version specified in the woocommerce_hooked_blocks_version option.
+ $valid_placements = array_filter(
+ $this->hooked_block_placements,
+ function ( $placement ) use ( $block_hooks_version ) {
+ $placement_version = isset( $placement['version'] ) ? $placement['version'] : null;
+ return is_null( $placement_version ) || ! is_null( $placement_version ) && version_compare( $block_hooks_version, $placement_version, '>=' );
+ }
+ );
+
+ if ( $context && ! empty( $valid_placements ) ) {
+ foreach ( $valid_placements as $placement ) {
if ( $placement['position'] === $position && $placement['anchor'] === $anchor_block ) {
// If an area has been specified for this placement.
diff --git a/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php b/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php
index c47b10aa1c2..ed9ffa1e58e 100644
--- a/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php
+++ b/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php
@@ -316,9 +316,13 @@ class BlockTemplateUtils {
$wp_template_part_filenames = array(
'checkout-header.html',
'mini-cart.html',
- 'product-filters-overlay.html',
);
+ if ( Features::is_enabled( 'experimental-blocks' ) ) {
+ $wp_template_part_filenames[] = 'product-filters.html';
+ $wp_template_part_filenames[] = 'product-filters-overlay.html';
+ }
+
/*
* This may return the blockified directory for wp_templates.
* At the moment every template file has a corresponding blockified file.
diff --git a/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php b/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php
index 4492c7cb08f..78c6cefe9e0 100644
--- a/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php
+++ b/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php
@@ -59,7 +59,8 @@ class ProductGalleryUtils {
wp_json_encode(
array(
'imageId' => $product_gallery_image_id,
- )
+ ),
+ JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
)
);
diff --git a/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php b/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php
index f7d2894ba22..10b1a60a933 100644
--- a/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php
+++ b/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php
@@ -88,7 +88,7 @@ final class ReserveStock {
try {
$items = array_filter(
$order->get_items(),
- function( $item ) {
+ function ( $item ) {
return $item->is_type( 'line_item' ) && $item->get_product() instanceof \WC_Product && $item->get_quantity() > 0;
}
);
diff --git a/plugins/woocommerce/src/Container.php b/plugins/woocommerce/src/Container.php
index 8a1d5d9fbf3..56d3b5b1e98 100644
--- a/plugins/woocommerce/src/Container.php
+++ b/plugins/woocommerce/src/Container.php
@@ -31,6 +31,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\UtilsC
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\BatchProcessingServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\LayoutTemplatesServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ComingSoonServiceProvider;
+use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\StatsServiceProvider;
/**
* PSR11 compliant dependency injection container for WooCommerce.
@@ -81,6 +82,7 @@ final class Container {
LoggingServiceProvider::class,
EnginesServiceProvider::class,
ComingSoonServiceProvider::class,
+ StatsServiceProvider::class,
);
/**
diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php
index 9e15258c336..aa804aef3a8 100644
--- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php
+++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php
@@ -1,6 +1,6 @@
get_id() ) . '" title="' . esc_attr( __( 'Preview', 'woocommerce' ) ) . '">' . esc_html( __( 'Preview', 'woocommerce' ) ) . '';
echo '
#' . esc_attr( $order->get_order_number() ) . ' ' . esc_html( $buyer ) . ' ';
}
+
+ // Used for showing date & status next to order number/buyer name on small screens.
+ echo '
';
+ $this->render_order_date_column( $order );
+ echo '
';
+ echo '
';
+ $this->render_order_status_column( $order );
+ echo '
';
}
/**
@@ -1056,14 +1064,14 @@ class ListTable extends WP_List_Table {
*/
private function get_order_status_label( WC_Order $order ): string {
$status_names = array(
- 'Pending payment' => __( 'The order has been received, but no payment has been made. Pending payment orders are generally awaiting customer action.', 'woocommerce' ),
- 'On hold' => __( 'The order is awaiting payment confirmation. Stock is reduced, but you need to confirm payment.', 'woocommerce' ),
- 'Processing' => __( 'Payment has been received (paid), and the stock has been reduced. The order is awaiting fulfillment.', 'woocommerce' ),
- 'Completed' => __( 'Order fulfilled and complete.', 'woocommerce' ),
- 'Failed' => __( 'The customer’s payment failed or was declined, and no payment has been successfully made.', 'woocommerce' ),
- 'Draft' => __( 'Draft orders are created when customers start the checkout process while the block version of the checkout is in place.', 'woocommerce' ),
- 'Canceled' => __( 'The order was canceled by an admin or the customer.', 'woocommerce' ),
- 'Refunded' => __( 'Orders are automatically put in the Refunded status when an admin or shop manager has fully refunded the order’s value after payment.', 'woocommerce' ),
+ 'pending' => __( 'The order has been received, but no payment has been made. Pending payment orders are generally awaiting customer action.', 'woocommerce' ),
+ 'on-hold' => __( 'The order is awaiting payment confirmation. Stock is reduced, but you need to confirm payment.', 'woocommerce' ),
+ 'processing' => __( 'Payment has been received (paid), and the stock has been reduced. The order is awaiting fulfillment.', 'woocommerce' ),
+ 'completed' => __( 'Order fulfilled and complete.', 'woocommerce' ),
+ 'failed' => __( 'The customer’s payment failed or was declined, and no payment has been successfully made.', 'woocommerce' ),
+ 'checkout-draft' => __( 'Draft orders are created when customers start the checkout process while the block version of the checkout is in place.', 'woocommerce' ),
+ 'cancelled' => __( 'The order was canceled by an admin or the customer.', 'woocommerce' ),
+ 'refunded' => __( 'Orders are automatically put in the Refunded status when an admin or shop manager has fully refunded the order’s value after payment.', 'woocommerce' ),
);
/**
@@ -1073,9 +1081,9 @@ class ListTable extends WP_List_Table {
* @param WC_Order $order Current order object.
* @since 9.1.0
*/
- $status_names = apply_filters( 'woocommerce_get_order_status_labels', $status_names );
+ $status_names = apply_filters( 'woocommerce_get_order_status_labels', $status_names, $order );
- $status_name = wc_get_order_status_name( $order->get_status() );
+ $status_name = $order->get_status();
return isset( $status_names[ $status_name ] ) ? $status_names[ $status_name ] : '';
}
diff --git a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php
index f81aa2c69ca..06f8ff673ab 100644
--- a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php
+++ b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php
@@ -93,10 +93,10 @@ class DefaultFreeExtensions {
$plugins = array(
'google-listings-and-ads' => array(
'min_php_version' => '7.4',
- 'name' => __( 'Google Listings & Ads', 'woocommerce' ),
+ 'name' => __( 'Google for WooCommerce', 'woocommerce' ),
'description' => sprintf(
/* translators: 1: opening product link tag. 2: closing link tag */
- __( 'Drive sales with %1$sGoogle Listings and Ads%2$s', 'woocommerce' ),
+ __( 'Drive sales with %1$sGoogle for WooCommerce%2$s', 'woocommerce' ),
'
',
' '
),
@@ -116,7 +116,7 @@ class DefaultFreeExtensions {
),
),
'google-listings-and-ads:alt' => array(
- 'name' => __( 'Google Listings & Ads', 'woocommerce' ),
+ 'name' => __( 'Google for WooCommerce', 'woocommerce' ),
'description' => __( 'Reach more shoppers and drive sales for your store. Integrate with Google to list your products for free and launch paid ad campaigns.', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/onboarding/google.svg', WC_PLUGIN_FILE ),
'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart',
@@ -862,7 +862,7 @@ class DefaultFreeExtensions {
'install_priority' => 1,
),
'google-listings-and-ads' => array(
- 'label' => __( 'Drive sales with Google Listings & Ads', 'woocommerce' ),
+ 'label' => __( 'Drive sales with Google for WooCommerce', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/core-profiler/logo-google.svg', WC_PLUGIN_FILE ),
'description' => __( 'Reach millions of active shoppers across Google with free product listings and ads.', 'woocommerce' ),
'learn_more_link' => 'https://woocommerce.com/products/google-listings-and-ads?utm_source=storeprofiler&utm_medium=product&utm_campaign=freefeatures',
diff --git a/plugins/woocommerce/src/Internal/Admin/WCPayPromotion/Init.php b/plugins/woocommerce/src/Internal/Admin/WCPayPromotion/Init.php
index 7d4f5c21819..8573c463735 100644
--- a/plugins/woocommerce/src/Internal/Admin/WCPayPromotion/Init.php
+++ b/plugins/woocommerce/src/Internal/Admin/WCPayPromotion/Init.php
@@ -114,7 +114,7 @@ class Init extends RemoteSpecsEngine {
$locale = get_user_locale();
$specs = self::get_specs();
- $results = EvaluateSuggestion::evaluate_specs( $specs );
+ $results = EvaluateSuggestion::evaluate_specs( $specs, array( 'source' => 'wc-wcpay-promotions' ) );
if ( count( $results['errors'] ) > 0 ) {
// Unlike payment gateway suggestions, we don't have a non-empty default set of promotions to fall back to.
diff --git a/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php b/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php
index b11d84f902f..d0a502d4844 100644
--- a/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php
+++ b/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php
@@ -24,7 +24,7 @@ namespace Automattic\WooCommerce\Internal\BatchProcessing;
/**
* Class BatchProcessingController
*
- * @package Automattic\WooCommerce\Internal\Updates.
+ * @package Automattic\WooCommerce\Internal\BatchProcessing.
*/
class BatchProcessingController {
/*
@@ -220,17 +220,19 @@ class BatchProcessingController {
* @return array Current state for the processor, or a "blank" state if none exists yet.
*/
private function get_process_details( BatchProcessorInterface $batch_processor ): array {
- return get_option(
- $this->get_processor_state_option_name( $batch_processor ),
- array(
- 'total_time_spent' => 0,
- 'current_batch_size' => $batch_processor->get_default_batch_size(),
- 'last_error' => null,
- 'recent_failures' => 0,
- 'batch_first_failure' => null,
- 'batch_last_failure' => null,
- )
+ $defaults = array(
+ 'total_time_spent' => 0,
+ 'current_batch_size' => $batch_processor->get_default_batch_size(),
+ 'last_error' => null,
+ 'recent_failures' => 0,
+ 'batch_first_failure' => null,
+ 'batch_last_failure' => null,
);
+
+ $process_details = get_option( $this->get_processor_state_option_name( $batch_processor ) );
+ $process_details = wp_parse_args( is_array( $process_details ) ? $process_details : array(), $defaults );
+
+ return $process_details;
}
/**
@@ -349,7 +351,15 @@ class BatchProcessingController {
* @return array List (of string) of the class names of the enqueued processors.
*/
public function get_enqueued_processors(): array {
- return get_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array() );
+ $enqueued_processors = get_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array() );
+
+ if ( ! is_array( $enqueued_processors ) ) {
+ $this->logger->error( 'Could not fetch list of processors. Clearing up queue.', array( 'source' => 'batch-processing' ) );
+ delete_option( self::ENQUEUED_PROCESSORS_OPTION_NAME );
+ $enqueued_processors = array();
+ }
+
+ return $enqueued_processors;
}
/**
diff --git a/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessorInterface.php b/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessorInterface.php
index 79dee9f8adc..1dff6d124a2 100644
--- a/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessorInterface.php
+++ b/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessorInterface.php
@@ -8,7 +8,7 @@ namespace Automattic\WooCommerce\Internal\BatchProcessing;
/**
* Interface BatchProcessorInterface
*
- * @package Automattic\WooCommerce\DataBase
+ * @package Automattic\WooCommerce\Internal\BatchProcessing
*/
interface BatchProcessorInterface {
diff --git a/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php b/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php
index d80f6a4c94a..31057ab11bd 100644
--- a/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php
+++ b/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php
@@ -5,7 +5,7 @@ use Automattic\WooCommerce\Admin\Features\Features;
/**
* Handles the template_include hook to determine whether the current page needs
- * to be replaced with a comiing soon screen.
+ * to be replaced with a coming soon screen.
*/
class ComingSoonRequestHandler {
@@ -185,7 +185,7 @@ class ComingSoonRequestHandler {
foreach ( $fonts_to_add as $font_to_add ) {
$found = false;
foreach ( $font_data as $font ) {
- if ( $font['name'] === $font_to_add['name'] ) {
+ if ( isset( $font['name'] ) && $font['name'] === $font_to_add['name'] ) {
$found = true;
break;
}
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php
index 3584b9106db..3345f6a61e5 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php
@@ -548,7 +548,7 @@ class CustomOrdersTableController {
$compatibility_info = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
$sync_complete = 0 === $this->data_synchronizer->get_current_orders_pending_sync_count();
$disabled = array();
- // Changing something here? You might also want to look at `enable|disable` functions in Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner.
+ // Changing something here? You might also want to look at `enable|disable` functions in Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner.
$incompatible_plugins = $this->plugin_util->get_items_considered_incompatible( 'custom_order_tables', $compatibility_info );
$incompatible_plugins = array_diff( $incompatible_plugins, $this->plugin_util->get_plugins_excluded_from_compatibility_ui() );
if ( count( $incompatible_plugins ) > 0 ) {
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
index c485bd15889..e0fb6c2b4c7 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
@@ -2586,7 +2586,7 @@ FROM $order_meta_table
* @param \WC_Order $order Order object.
*/
public function update( &$order ) {
- $previous_status = ArrayUtil::get_value_or_default( $order->get_data(), 'status' );
+ $previous_status = ArrayUtil::get_value_or_default( $order->get_data(), 'status', 'new' );
// Before updating, ensure date paid is set if missing.
if (
@@ -2621,8 +2621,15 @@ FROM $order_meta_table
$order->apply_changes();
$this->clear_caches( $order );
- // For backwards compatibility, moving a draft order to a valid status triggers the 'woocommerce_new_order' hook.
- if ( ! empty( $changes['status'] ) && in_array( $previous_status, array( 'new', 'auto-draft', 'draft', 'checkout-draft' ), true ) ) {
+ $draft_statuses = array( 'new', 'auto-draft', 'draft', 'checkout-draft' );
+
+ // For backwards compatibility, this hook should be fired only if the new status is not one of the draft statuses and the previous status was one of the draft statuses.
+ if (
+ ! empty( $changes['status'] )
+ && $changes['status'] !== $previous_status
+ && ! in_array( $changes['status'], $draft_statuses, true )
+ && in_array( $previous_status, $draft_statuses, true )
+ ) {
do_action( 'woocommerce_new_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
return;
}
diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php
index 92c338b296d..8e02dfd0d3f 100644
--- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php
+++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php
@@ -5,7 +5,7 @@
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
-use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner;
+use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/LoggingServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/LoggingServiceProvider.php
index 58946dac50c..89f15221230 100644
--- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/LoggingServiceProvider.php
+++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/LoggingServiceProvider.php
@@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\Admin\Logging\{ PageController, Settings };
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
+use Automattic\WooCommerce\Internal\Logging\RemoteLogger;
/**
* LoggingServiceProvider class.
@@ -19,6 +20,7 @@ class LoggingServiceProvider extends AbstractServiceProvider {
FileController::class,
PageController::class,
Settings::class,
+ RemoteLogger::class,
);
/**
@@ -37,5 +39,7 @@ class LoggingServiceProvider extends AbstractServiceProvider {
);
$this->share( Settings::class );
+
+ $this->share( RemoteLogger::class );
}
}
diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php
index 6b37c82918b..09837cf54b4 100644
--- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php
+++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php
@@ -8,7 +8,7 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Caches\OrderCacheController;
-use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner;
+use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableRefundDataStore;
diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/StatsServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/StatsServiceProvider.php
new file mode 100644
index 00000000000..793fdf9418b
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/StatsServiceProvider.php
@@ -0,0 +1,33 @@
+add( McStats::class );
+ }
+}
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index c7b33c14e36..c332e769f65 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -247,6 +247,17 @@ class FeaturesController {
'is_legacy' => true,
'option_key' => CustomOrdersTableController::HPOS_FTS_INDEX_OPTION,
),
+ 'remote_logging' => array(
+ 'name' => __( 'Remote Logging', 'woocommerce' ),
+ 'description' => __(
+ 'Enable this feature to log errors and related data to Automattic servers for debugging purposes and to improve WooCommerce',
+ 'woocommerce'
+ ),
+ 'enabled_by_default' => false,
+ 'disable_ui' => true,
+ 'is_legacy' => false,
+ 'is_experimental' => true,
+ ),
);
foreach ( $legacy_features as $slug => $definition ) {
diff --git a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/ProductVariationTemplate.php b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/ProductVariationTemplate.php
index 665ef1be5e9..1fc49acd1d1 100644
--- a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/ProductVariationTemplate.php
+++ b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/ProductVariationTemplate.php
@@ -339,7 +339,8 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
'order' => 20,
'attributes' => array(
'property' => 'global_unique_id',
- 'label' => __( 'GTIN, UPC, EAN or ISBN', 'woocommerce' ),
+ // translators: %1$s GTIN %2$s UPC %3$s EAN %4$s ISBN.
+ 'label' => sprintf( __( '%1$s, %2$s, %3$s, or %4$s', 'woocommerce' ), '
' . esc_html__( 'GTIN', 'woocommerce' ) . ' ', '
' . esc_html__( 'UPC', 'woocommerce' ) . ' ', '
' . esc_html__( 'EAN', 'woocommerce' ) . ' ', '
' . esc_html__( 'ISBN', 'woocommerce' ) . ' ' ),
'tooltip' => __( 'Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.', 'woocommerce' ),
),
)
diff --git a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php
index 18a36daa681..79dde35c56d 100644
--- a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php
+++ b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php
@@ -765,7 +765,8 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'order' => 20,
'attributes' => array(
'property' => 'global_unique_id',
- 'label' => __( 'GTIN, UPC, EAN or ISBN', 'woocommerce' ),
+ // translators: %1$s GTIN %2$s UPC %3$s EAN %4$s ISBN.
+ 'label' => sprintf( __( '%1$s, %2$s, %3$s, or %4$s', 'woocommerce' ), '
' . esc_html__( 'GTIN', 'woocommerce' ) . ' ', '
' . esc_html__( 'UPC', 'woocommerce' ) . ' ', '
' . esc_html__( 'EAN', 'woocommerce' ) . ' ', '
' . esc_html__( 'ISBN', 'woocommerce' ) . ' ' ),
'tooltip' => __( 'Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.', 'woocommerce' ),
),
'disableConditions' => array(
diff --git a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php
new file mode 100644
index 00000000000..6d9cb9cf146
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php
@@ -0,0 +1,128 @@
+is_tracking_opted_in() ) {
+ return false;
+ }
+
+ if ( ! $this->is_variant_assignment_allowed() ) {
+ return false;
+ }
+
+ if ( ! $this->is_latest_woocommerce_version() ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if the user has opted into tracking/logging.
+ *
+ * @return bool
+ */
+ private function is_tracking_opted_in() {
+ return 'yes' === get_option( 'woocommerce_allow_tracking', 'no' );
+ }
+
+ /**
+ * Check if the store is allowed to log based on the variant assignment percentage.
+ *
+ * @return bool
+ */
+ private function is_variant_assignment_allowed() {
+ $assignment = get_option( 'woocommerce_remote_variant_assignment', 0 );
+ return ( $assignment <= 12 ); // Considering 10% of the 0-120 range.
+ }
+
+ /**
+ * Check if the current WooCommerce version is the latest.
+ *
+ * @return bool
+ */
+ private function is_latest_woocommerce_version() {
+ $latest_wc_version = $this->fetch_latest_woocommerce_version();
+
+ if ( is_null( $latest_wc_version ) ) {
+ return false;
+ }
+
+ return version_compare( WC()->version, $latest_wc_version, '>=' );
+ }
+
+ /**
+ * Fetch the latest WooCommerce version using the WordPress API and cache it.
+ *
+ * @return string|null
+ */
+ private function fetch_latest_woocommerce_version() {
+ $cached_version = get_transient( self::WC_LATEST_VERSION_TRANSIENT );
+ if ( $cached_version ) {
+ return $cached_version;
+ }
+
+ $retry_count = get_transient( self::FETCH_LATEST_VERSION_RETRY );
+ if ( false === $retry_count || ! is_numeric( $retry_count ) ) {
+ $retry_count = 0;
+ }
+
+ if ( $retry_count >= 3 ) {
+ return null;
+ }
+
+ if ( ! function_exists( 'plugins_api' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
+ }
+ // Fetch the latest version from the WordPress API.
+ $plugin_info = plugins_api( 'plugin_information', array( 'slug' => 'woocommerce' ) );
+
+ if ( is_wp_error( $plugin_info ) ) {
+ ++$retry_count;
+ set_transient( self::FETCH_LATEST_VERSION_RETRY, $retry_count, HOUR_IN_SECONDS );
+ return null;
+ }
+
+ if ( ! empty( $plugin_info->version ) ) {
+ $latest_version = $plugin_info->version;
+ set_transient( self::WC_LATEST_VERSION_TRANSIENT, $latest_version, WEEK_IN_SECONDS );
+ delete_transient( self::FETCH_LATEST_VERSION_RETRY );
+ return $latest_version;
+ }
+
+ return null;
+ }
+}
diff --git a/plugins/woocommerce/src/Internal/McStats.php b/plugins/woocommerce/src/Internal/McStats.php
new file mode 100644
index 00000000000..89320e1ee6a
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/McStats.php
@@ -0,0 +1,63 @@
+get_current_stats();
+ if ( isset( $stats[ $group_name ] ) && ! empty( $stats[ $group_name ] ) ) {
+ return array( "x_woocommerce-{$group_name}" => implode( ',', $stats[ $group_name ] ) );
+ }
+ return array();
+ }
+
+ /**
+ * Outputs the tracking pixels for the current stats and empty the stored stats from the object
+ *
+ * @return void
+ */
+ public function do_stats() {
+ if ( ! \WC_Site_Tracking::is_tracking_enabled() ) {
+ return;
+ }
+
+ parent::do_stats();
+ }
+
+ /**
+ * Runs stats code for a one-off, server-side.
+ *
+ * @param string $url string The URL to be pinged. Should include `x_woocommerce-{$group}={$stats}` or whatever we want to store.
+ *
+ * @return bool If it worked.
+ */
+ public function do_server_side_stat( $url ) {
+ if ( ! \WC_Site_Tracking::is_tracking_enabled() ) {
+ return false;
+ }
+
+ return parent::do_server_side_stat( $url );
+ }
+}
diff --git a/plugins/woocommerce/src/Internal/ProductAttributesLookup/LookupDataStore.php b/plugins/woocommerce/src/Internal/ProductAttributesLookup/LookupDataStore.php
index e9ba76b3b63..cf0b6325751 100644
--- a/plugins/woocommerce/src/Internal/ProductAttributesLookup/LookupDataStore.php
+++ b/plugins/woocommerce/src/Internal/ProductAttributesLookup/LookupDataStore.php
@@ -431,9 +431,14 @@ class LookupDataStore {
* Create all the necessary lookup data for a given variation.
*
* @param \WC_Product_Variation $variation The variation to create entries for.
+ * @throws \Exception Can't retrieve the details of the parent product.
*/
private function create_data_for_variation( \WC_Product_Variation $variation ) {
$main_product = WC()->call_function( 'wc_get_product', $variation->get_parent_id() );
+ if ( false === $main_product ) {
+ // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
+ throw new \Exception( "The product is a variation, and the retrieval of data for the parent product (id {$variation->get_parent_id()}) failed." );
+ }
$product_attributes_data = $this->get_attribute_taxonomies( $main_product );
$variation_attributes_data = array_filter(
diff --git a/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingEngine.php b/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingEngine.php
index 5bc479e3fd6..890856e062c 100644
--- a/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingEngine.php
+++ b/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingEngine.php
@@ -198,8 +198,34 @@ class ReceiptRenderingEngine {
*/
$data['css'] = apply_filters( 'woocommerce_printable_order_receipt_css', $css, $order );
+ $default_template_path = __DIR__ . '/Templates/order-receipt.php';
+
+ /**
+ * Filter the order receipt template path.
+ *
+ * @since 9.2.0
+ * @hook wc_get_template
+ * @param string $template The template path.
+ * @param string $template_name The template name.
+ * @param array $args The available data for the template.
+ * @param string $template_path The template path.
+ * @param string $default_path The default template path.
+ */
+ $template_path = apply_filters(
+ 'wc_get_template',
+ $default_template_path,
+ 'ReceiptRendering/order-receipt.php',
+ $data,
+ $default_template_path,
+ $default_template_path
+ );
+
+ if ( ! file_exists( $template_path ) ) {
+ $template_path = $default_template_path;
+ }
+
ob_start();
- include __DIR__ . '/Templates/order-receipt.php';
+ include $template_path;
$rendered_template = ob_get_contents();
ob_end_clean();
diff --git a/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingRestController.php b/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingRestController.php
index 3674da31e7b..d871559c360 100644
--- a/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingRestController.php
+++ b/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingRestController.php
@@ -6,8 +6,8 @@ use Automattic\WooCommerce\Internal\TransientFiles\TransientFilesEngine;
use \WP_REST_Server;
use \WP_REST_Request;
use \WP_Error;
-use \InvalidArgumentException;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
+use Automattic\WooCommerce\Internal\RestApiControllerBase;
/**
* Controller for the REST endpoints associated to the receipt rendering engine.
diff --git a/plugins/woocommerce/src/Internal/RestApiControllerBase.php b/plugins/woocommerce/src/Internal/RestApiControllerBase.php
index a01fe7a682a..ca7921fcdf9 100644
--- a/plugins/woocommerce/src/Internal/RestApiControllerBase.php
+++ b/plugins/woocommerce/src/Internal/RestApiControllerBase.php
@@ -1,6 +1,6 @@
substr_compare( $plugin, '/woocommerce.php', -strlen( '/woocommerce.php' ) ) === 0 ) );
+ $active_valid_plugins = wc_get_container()->get( PluginUtil::class )->get_all_active_valid_plugins();
+
+ return ! empty(
+ array_filter(
+ $active_valid_plugins,
+ fn( $plugin ) => substr_compare( $plugin, '/woocommerce.php', -strlen( '/woocommerce.php' ) ) === 0
+ )
+ );
}
/**
diff --git a/plugins/woocommerce/src/Internal/Utilities/Users.php b/plugins/woocommerce/src/Internal/Utilities/Users.php
index 34880bac135..8c333498563 100644
--- a/plugins/woocommerce/src/Internal/Utilities/Users.php
+++ b/plugins/woocommerce/src/Internal/Utilities/Users.php
@@ -106,4 +106,66 @@ class Users {
*/
return (bool) apply_filters( 'woocommerce_order_email_verification_required', $email_verification_required, $order, $context );
}
+
+ /**
+ * Site-specific method of retrieving the requested user meta.
+ *
+ * This is a multisite-aware wrapper around WordPress's own `get_user_meta()` function, and works by prefixing the
+ * supplied meta key with a blog-specific meta key.
+ *
+ * @param int $user_id User ID.
+ * @param string $key Optional. The meta key to retrieve. By default, returns data for all keys.
+ * @param bool $single Optional. Whether to return a single value. This parameter has no effect if `$key` is not
+ * specified. Default false.
+ *
+ * @return mixed An array of values if `$single` is false. The value of meta data field if `$single` is true.
+ * False for an invalid `$user_id` (non-numeric, zero, or negative value). An empty string if a valid
+ * but non-existing user ID is passed.
+ */
+ public static function get_site_user_meta( int $user_id, string $key = '', bool $single = false ) {
+ global $wpdb;
+ $site_specific_key = $key . '_' . rtrim( $wpdb->get_blog_prefix( get_current_blog_id() ), '_' );
+ return get_user_meta( $user_id, $site_specific_key, true );
+ }
+
+ /**
+ * Site-specific means of updating user meta.
+ *
+ * This is a multisite-aware wrapper around WordPress's own `update_user_meta()` function, and works by prefixing
+ * the supplied meta key with a blog-specific meta key.
+ *
+ * @param int $user_id User ID.
+ * @param string $meta_key Metadata key.
+ * @param mixed $meta_value Metadata value. Must be serializable if non-scalar.
+ * @param mixed $prev_value Optional. Previous value to check before updating. If specified, only update existing
+ * metadata entries with this value. Otherwise, update all entries. Default empty.
+ *
+ * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure or if the value
+ * passed to the function is the same as the one that is already in the database.
+ */
+ public static function update_site_user_meta( int $user_id, string $meta_key, $meta_value, $prev_value = '' ) {
+ global $wpdb;
+ $site_specific_key = $meta_key . '_' . rtrim( $wpdb->get_blog_prefix( get_current_blog_id() ), '_' );
+ return update_user_meta( $user_id, $site_specific_key, $meta_value, $prev_value );
+ }
+
+ /**
+ * Site-specific means of deleting user meta.
+ *
+ * This is a multisite-aware wrapper around WordPress's own `delete_user_meta()` function, and works by prefixing
+ * the supplied meta key with a blog-specific meta key.
+ *
+ * @param int $user_id User ID.
+ * @param string $meta_key Metadata name.
+ * @param mixed $meta_value Optional. Metadata value. If provided, rows will only be removed that match the value.
+ * Must be serializable if non-scalar. Default empty.
+ *
+ * @return bool True on success, false on failure.
+ * /
+ */
+ public static function delete_site_user_meta( $user_id, $meta_key, $meta_value = '' ) {
+ global $wpdb;
+ $site_specific_key = $meta_key . '_' . rtrim( $wpdb->get_blog_prefix(), '_' );
+ return delete_user_meta( $user_id, $site_specific_key, $meta_value );
+ }
}
diff --git a/plugins/woocommerce/src/StoreApi/Formatters/MoneyFormatter.php b/plugins/woocommerce/src/StoreApi/Formatters/MoneyFormatter.php
index e4a61272228..c6ce0f3637c 100644
--- a/plugins/woocommerce/src/StoreApi/Formatters/MoneyFormatter.php
+++ b/plugins/woocommerce/src/StoreApi/Formatters/MoneyFormatter.php
@@ -8,13 +8,24 @@ namespace Automattic\WooCommerce\StoreApi\Formatters;
*/
class MoneyFormatter implements FormatterInterface {
/**
- * Format a given value and return the result.
+ * Format a given price value and return the result as a string without decimals.
*
- * @param mixed $value Value to format.
- * @param array $options Options that influence the formatting.
- * @return mixed
+ * @param int|float|string $value Value to format. Int is allowed, as it may also represent a valid price.
+ * @param array $options Options that influence the formatting.
+ * @return string
*/
public function format( $value, array $options = [] ) {
+
+ if ( ! is_int( $value ) && ! is_string( $value ) && ! is_float( $value ) ) {
+ wc_doing_it_wrong(
+ __FUNCTION__,
+ 'Function expects a $value arg of type INT, STRING or FLOAT.',
+ '9.2'
+ );
+
+ return '';
+ }
+
$options = wp_parse_args(
$options,
[
@@ -23,12 +34,20 @@ class MoneyFormatter implements FormatterInterface {
]
);
- return (string) intval(
- round(
- ( (float) wc_format_decimal( $value ) ) * ( 10 ** absint( $options['decimals'] ) ),
- 0,
- absint( $options['rounding_mode'] )
- )
- );
+ // Ensure rounding mode is valid.
+ $rounding_modes = [ PHP_ROUND_HALF_UP, PHP_ROUND_HALF_DOWN, PHP_ROUND_HALF_EVEN, PHP_ROUND_HALF_ODD ];
+ $options['rounding_mode'] = absint( $options['rounding_mode'] );
+ if ( ! in_array( $options['rounding_mode'], $rounding_modes, true ) ) {
+ $options['rounding_mode'] = PHP_ROUND_HALF_UP;
+ }
+
+ $value = floatval( $value );
+
+ // Remove the price decimal points for rounding purposes.
+ $value = $value * pow( 10, absint( $options['decimals'] ) );
+ $value = round( $value, 0, $options['rounding_mode'] );
+
+ // This ensures returning the value as a string without decimal points ready for price parsing.
+ return wc_format_decimal( $value, 0, true );
}
}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php
index 797df5f9eca..a760664327f 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php
@@ -105,7 +105,6 @@ abstract class AbstractCartRoute extends AbstractRoute {
*/
public function get_response( \WP_REST_Request $request ) {
$this->load_cart_session( $request );
- $this->cart_controller->calculate_totals();
$response = null;
$nonce_check = $this->requires_nonce( $request ) ? $this->check_nonce( $request ) : null;
@@ -332,13 +331,11 @@ abstract class AbstractCartRoute extends AbstractRoute {
* @return \WP_Error WP Error object.
*/
protected function get_route_error_response( $error_code, $error_message, $http_status_code = 500, $additional_data = [] ) {
-
$additional_data['status'] = $http_status_code;
// If there was a conflict, return the cart so the client can resolve it.
if ( 409 === $http_status_code ) {
- $cart = $this->cart_controller->get_cart_instance();
- $additional_data['cart'] = $this->cart_schema->get_item_response( $cart );
+ $additional_data['cart'] = $this->cart_schema->get_item_response( $this->cart_controller->get_cart_for_response() );
}
return new \WP_Error( $error_code, $error_message, $additional_data );
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractRoute.php b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractRoute.php
index 00fff318066..87600688478 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractRoute.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractRoute.php
@@ -6,8 +6,6 @@ use Automattic\WooCommerce\StoreApi\Routes\RouteInterface;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidCartException;
use Automattic\WooCommerce\StoreApi\Schemas\v1\AbstractSchema;
-use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
-use Automattic\WooCommerce\Blocks\Package;
use WP_Error;
/**
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Cart.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Cart.php
index 8cab713b2d7..3ec380321d0 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Cart.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Cart.php
@@ -56,6 +56,6 @@ class Cart extends AbstractCartRoute {
* @return \WP_REST_Response
*/
protected function get_route_response( \WP_REST_Request $request ) {
- return rest_ensure_response( $this->schema->get_item_response( $this->cart_controller->get_cart_instance() ) );
+ return rest_ensure_response( $this->schema->get_item_response( $this->cart_controller->get_cart_for_response() ) );
}
}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartAddItem.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartAddItem.php
index 7a7191070d1..5237160c6b0 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartAddItem.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartAddItem.php
@@ -95,11 +95,9 @@ class CartAddItem extends AbstractCartRoute {
protected function get_route_post_response( \WP_REST_Request $request ) {
// Do not allow key to be specified during creation.
if ( ! empty( $request['key'] ) ) {
- throw new RouteException( 'woocommerce_rest_cart_item_exists', __( 'Cannot create an existing cart item.', 'woocommerce' ), 400 );
+ throw new RouteException( 'woocommerce_rest_cart_item_exists', esc_html__( 'Cannot create an existing cart item.', 'woocommerce' ), 400 );
}
- $cart = $this->cart_controller->get_cart_instance();
-
/**
* Filters cart item data sent via the API before it is passed to the cart controller.
*
@@ -128,7 +126,7 @@ class CartAddItem extends AbstractCartRoute {
$this->cart_controller->add_to_cart( $add_to_cart_data );
- $response = rest_ensure_response( $this->schema->get_item_response( $cart ) );
+ $response = rest_ensure_response( $this->schema->get_item_response( $this->cart_controller->get_cart_for_response() ) );
$response->set_status( 201 );
return $response;
}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartApplyCoupon.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartApplyCoupon.php
index 2d277395734..56fffb558b2 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartApplyCoupon.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartApplyCoupon.php
@@ -64,7 +64,7 @@ class CartApplyCoupon extends AbstractCartRoute {
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
if ( ! wc_coupons_enabled() ) {
- throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', __( 'Coupons are disabled.', 'woocommerce' ), 404 );
+ throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', esc_html__( 'Coupons are disabled.', 'woocommerce' ), 404 );
}
$coupon_code = wc_format_coupon_code( wp_unslash( $request['code'] ) );
@@ -72,10 +72,9 @@ class CartApplyCoupon extends AbstractCartRoute {
try {
$this->cart_controller->apply_coupon( $coupon_code );
} catch ( \WC_REST_Exception $e ) {
- throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() );
+ throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
- $cart = $this->cart_controller->get_cart_instance();
- return rest_ensure_response( $this->schema->get_item_response( $cart ) );
+ return rest_ensure_response( $this->schema->get_item_response( $this->cart_controller->get_cart_for_response() ) );
}
}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartCouponsByCode.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartCouponsByCode.php
index 97fd807c520..879f20534f3 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartCouponsByCode.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartCouponsByCode.php
@@ -79,7 +79,7 @@ class CartCouponsByCode extends AbstractCartRoute {
*/
protected function get_route_response( \WP_REST_Request $request ) {
if ( ! $this->cart_controller->has_coupon( $request['code'] ) ) {
- throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', __( 'Coupon does not exist in the cart.', 'woocommerce' ), 404 );
+ throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', esc_html__( 'Coupon does not exist in the cart.', 'woocommerce' ), 404 );
}
return $this->prepare_item_for_response( $request['code'], $request );
@@ -94,13 +94,11 @@ class CartCouponsByCode extends AbstractCartRoute {
*/
protected function get_route_delete_response( \WP_REST_Request $request ) {
if ( ! $this->cart_controller->has_coupon( $request['code'] ) ) {
- throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', __( 'Coupon does not exist in the cart.', 'woocommerce' ), 404 );
+ throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', esc_html__( 'Coupon does not exist in the cart.', 'woocommerce' ), 404 );
}
$cart = $this->cart_controller->get_cart_instance();
-
$cart->remove_coupon( $request['code'] );
- $cart->calculate_totals();
return new \WP_REST_Response( null, 204 );
}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartRemoveCoupon.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartRemoveCoupon.php
index 4050c211c0d..2bd81b283be 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartRemoveCoupon.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartRemoveCoupon.php
@@ -64,24 +64,24 @@ class CartRemoveCoupon extends AbstractCartRoute {
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
if ( ! wc_coupons_enabled() ) {
- throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', __( 'Coupons are disabled.', 'woocommerce' ), 404 );
+ throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', esc_html__( 'Coupons are disabled.', 'woocommerce' ), 404 );
}
$cart = $this->cart_controller->get_cart_instance();
$coupon_code = wc_format_coupon_code( $request['code'] );
$coupon = new \WC_Coupon( $coupon_code );
+ $discounts = new \WC_Discounts( $cart );
- if ( $coupon->get_code() !== $coupon_code || ! $coupon->is_valid() ) {
- throw new RouteException( 'woocommerce_rest_cart_coupon_error', __( 'Invalid coupon code.', 'woocommerce' ), 400 );
+ if ( $coupon->get_code() !== $coupon_code || is_wp_error( $discounts->is_coupon_valid( $coupon ) ) ) {
+ throw new RouteException( 'woocommerce_rest_cart_coupon_error', esc_html__( 'Invalid coupon code.', 'woocommerce' ), 400 );
}
if ( ! $this->cart_controller->has_coupon( $coupon_code ) ) {
- throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', __( 'Coupon cannot be removed because it is not already applied to the cart.', 'woocommerce' ), 409 );
+ throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', esc_html__( 'Coupon cannot be removed because it is not already applied to the cart.', 'woocommerce' ), 409 );
}
$cart = $this->cart_controller->get_cart_instance();
$cart->remove_coupon( $coupon_code );
- $cart->calculate_totals();
return rest_ensure_response( $this->schema->get_item_response( $cart ) );
}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartSelectShippingRate.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartSelectShippingRate.php
index 44f2d534d76..7fd63372c6e 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartSelectShippingRate.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartSelectShippingRate.php
@@ -70,11 +70,11 @@ class CartSelectShippingRate extends AbstractCartRoute {
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
if ( ! wc_shipping_enabled() ) {
- throw new RouteException( 'woocommerce_rest_shipping_disabled', __( 'Shipping is disabled.', 'woocommerce' ), 404 );
+ throw new RouteException( 'woocommerce_rest_shipping_disabled', esc_html__( 'Shipping is disabled.', 'woocommerce' ), 404 );
}
if ( ! isset( $request['rate_id'] ) ) {
- throw new RouteException( 'woocommerce_rest_cart_missing_rate_id', __( 'Invalid Rate ID.', 'woocommerce' ), 400 );
+ throw new RouteException( 'woocommerce_rest_cart_missing_rate_id', esc_html__( 'Invalid Rate ID.', 'woocommerce' ), 400 );
}
$cart = $this->cart_controller->get_cart_instance();
@@ -90,7 +90,7 @@ class CartSelectShippingRate extends AbstractCartRoute {
}
}
} catch ( \WC_Rest_Exception $e ) {
- throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() );
+ throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
/**
@@ -107,7 +107,6 @@ class CartSelectShippingRate extends AbstractCartRoute {
*/
do_action( 'woocommerce_store_api_cart_select_shipping_rate', $package_id, $rate_id, $request );
- $cart->calculate_shipping();
$cart->calculate_totals();
return rest_ensure_response( $this->cart_schema->get_item_response( $cart ) );
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php
index 86fa79596d0..782298d2d9c 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php
@@ -2,7 +2,6 @@
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
-use Automattic\WooCommerce\StoreApi\Utilities\ValidationUtils;
/**
* CartUpdateCustomer class.
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
index 5d6f34bb1bf..dbdf838fb28 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
@@ -2,11 +2,9 @@
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
-use Automattic\WooCommerce\StoreApi\Exceptions\InvalidStockLevelsInCartException;
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidCartException;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
-use Automattic\WooCommerce\Checkout\Helpers\ReserveStock;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStockException;
use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait;
use Automattic\WooCommerce\Utilities\RestApiUtil;
@@ -88,7 +86,7 @@ class Checkout extends AbstractCartRoute {
'permission_callback' => '__return_true',
'args' => array_merge(
[
- 'payment_data' => [
+ 'payment_data' => [
'description' => __( 'Data to pass through to the payment method when processing payment.', 'woocommerce' ),
'type' => 'array',
'items' => [
@@ -103,6 +101,10 @@ class Checkout extends AbstractCartRoute {
],
],
],
+ 'customer_password' => [
+ 'description' => __( 'Customer password for new accounts, if applicable.', 'woocommerce' ),
+ 'type' => 'string',
+ ],
],
$this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE )
),
@@ -121,7 +123,6 @@ class Checkout extends AbstractCartRoute {
*/
public function get_response( \WP_REST_Request $request ) {
$this->load_cart_session( $request );
- $this->cart_controller->calculate_totals();
$response = null;
$nonce_check = $this->requires_nonce( $request ) ? $this->check_nonce( $request ) : null;
@@ -144,6 +145,11 @@ class Checkout extends AbstractCartRoute {
if ( is_wp_error( $response ) ) {
$response = $this->error_to_response( $response );
+
+ // If we encountered an exception, free up stock.
+ if ( $this->order ) {
+ wc_release_stock_for_order( $this->order );
+ }
}
return $this->add_response_headers( $response );
@@ -178,7 +184,6 @@ class Checkout extends AbstractCartRoute {
* 5. Process Payment
*
* @throws RouteException On error.
- * @throws InvalidStockLevelsInCartException On error.
*
* @param \WP_REST_Request $request Request object.
*
@@ -186,35 +191,62 @@ class Checkout extends AbstractCartRoute {
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
/**
- * Validate items etc are allowed in the order before the order is processed. This will fix violations and tell
- * the customer.
+ * Before triggering validation, ensure totals are current and in turn, things such as shipping costs are present.
+ * This is so plugins that validate other cart data (e.g. conditional shipping and payments) can access this data.
+ */
+ $this->cart_controller->calculate_totals();
+
+ /**
+ * Validate items and fix violations before the order is processed.
*/
$this->cart_controller->validate_cart();
/**
- * Obtain Draft Order and process request data.
- *
- * Note: Customer data is persisted from the request first so that OrderController::update_addresses_from_cart
+ * Persist customer session data from the request first so that OrderController::update_addresses_from_cart
* uses the up to date customer address.
*/
$this->update_customer_from_request( $request );
- $this->create_or_update_draft_order( $request );
- $this->update_order_from_request( $request );
/**
- * Process customer data.
- *
- * Update order with customer details, and sign up a user account as necessary.
+ * Create (or update) Draft Order and process request data.
*/
+ $this->create_or_update_draft_order( $request );
+ $this->update_order_from_request( $request );
$this->process_customer( $request );
/**
- * Validate order.
- *
- * This logic ensures the order is valid before payment is attempted.
+ * Validate updated order before payment is attempted.
*/
$this->order_controller->validate_order_before_payment( $this->order );
+ /**
+ * Reserve stock for the order.
+ *
+ * In the shortcode based checkout, when POSTing the checkout form the order would be created and fire the
+ * `woocommerce_checkout_order_created` action. This in turn would trigger the `wc_reserve_stock_for_order`
+ * function so that stock would be held pending payment.
+ *
+ * Via the block based checkout and Store API we already have a draft order, but when POSTing to the /checkout
+ * endpoint we do the same; reserve stock for the order to allow time to process payment.
+ *
+ * Note, stock is only "held" while the order has the status wc-checkout-draft or pending. Stock is freed when
+ * the order changes status, or there is an exception.
+ *
+ * @see ReserveStock::get_query_for_reserved_stock()
+ *
+ * @since 9.2 Stock is no longer held for all draft orders, nor on non-POST requests. See https://github.com/woocommerce/woocommerce/issues/44231
+ * @since 9.2 Uses wc_reserve_stock_for_order() instead of using the ReserveStock class directly.
+ */
+ try {
+ wc_reserve_stock_for_order( $this->order );
+ } catch ( ReserveStockException $e ) {
+ throw new RouteException(
+ esc_html( $e->getErrorCode() ),
+ esc_html( $e->getMessage() ),
+ esc_html( $e->getCode() )
+ );
+ }
+
wc_do_deprecated_action(
'__experimental_woocommerce_blocks_checkout_order_processed',
array(
@@ -393,24 +425,6 @@ class Checkout extends AbstractCartRoute {
// Store order ID to session.
$this->set_draft_order_id( $this->order->get_id() );
-
- /**
- * Try to reserve stock for the order.
- *
- * If creating a draft order on checkout entry, set the timeout to 10 mins.
- * If POSTing to the checkout (attempting to pay), set the timeout to 60 mins (using the woocommerce_hold_stock_minutes option).
- */
- try {
- $reserve_stock = new ReserveStock();
- $duration = $request->get_method() === 'POST' ? (int) get_option( 'woocommerce_hold_stock_minutes', 60 ) : 10;
- $reserve_stock->reserve_stock_for_order( $this->order, $duration );
- } catch ( ReserveStockException $e ) {
- throw new RouteException(
- $e->getErrorCode(),
- $e->getMessage(),
- $e->getCode()
- );
- }
}
/**
@@ -524,7 +538,8 @@ class Checkout extends AbstractCartRoute {
$customer_id = $this->create_customer_account(
$request['billing_address']['email'],
$request['billing_address']['first_name'],
- $request['billing_address']['last_name']
+ $request['billing_address']['last_name'],
+ $request['customer_password']
);
// Associate customer with the order. This is done before login to ensure the order is associated with
@@ -547,13 +562,23 @@ class Checkout extends AbstractCartRoute {
case 'registration-error-invalid-email':
throw new RouteException(
'registration-error-invalid-email',
- __( 'Please provide a valid email address.', 'woocommerce' ),
+ esc_html__( 'Please provide a valid email address.', 'woocommerce' ),
400
);
case 'registration-error-email-exists':
throw new RouteException(
'registration-error-email-exists',
- __( 'An account is already registered with your email address. Please log in before proceeding.', 'woocommerce' ),
+ sprintf(
+ // Translators: %s Email address.
+ esc_html__( 'An account is already registered with %s. Please log in or use a different email address.', 'woocommerce' ),
+ esc_html( $request['billing_address']['email'] )
+ ),
+ 400
+ );
+ case 'registration-error-empty-password':
+ throw new RouteException(
+ 'registration-error-empty-password',
+ esc_html__( 'Please create a password for your account.', 'woocommerce' ),
400
);
}
@@ -608,10 +633,11 @@ class Checkout extends AbstractCartRoute {
* @param string $user_email The email address to use for the new account.
* @param string $first_name The first name to use for the new account.
* @param string $last_name The last name to use for the new account.
+ * @param string $password The password to use for the new account. If empty, a password will be generated.
*
* @return int User id if successful
*/
- private function create_customer_account( $user_email, $first_name, $last_name ) {
+ private function create_customer_account( $user_email, $first_name, $last_name, $password = '' ) {
if ( empty( $user_email ) || ! is_email( $user_email ) ) {
throw new \Exception( 'registration-error-invalid-email' );
}
@@ -620,11 +646,20 @@ class Checkout extends AbstractCartRoute {
throw new \Exception( 'registration-error-email-exists' );
}
- $username = wc_create_new_customer_username( $user_email );
+ // Handle password creation if not provided.
+ if ( empty( $password ) ) {
+ $password = wp_generate_password();
+ $password_generated = true;
+ } else {
+ $password_generated = false;
+ }
- // Handle password creation.
- $password = wp_generate_password();
- $password_generated = true;
+ // This ensures `wp_generate_password` returned something (it is filterable and could be empty string).
+ if ( empty( $password ) ) {
+ throw new \Exception( 'registration-error-empty-password' );
+ }
+
+ $username = wc_create_new_customer_username( $user_email );
// Use WP_Error to handle registration errors.
$errors = new \WP_Error();
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Patterns.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Patterns.php
index 41d16a07ed5..96a0e37ce34 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Patterns.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Patterns.php
@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
+use Automattic\WooCommerce\Blocks\BlockPatterns;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Patterns\PTKClient;
use Automattic\WooCommerce\Blocks\Patterns\PTKPatternsStore;
@@ -114,7 +115,11 @@ class Patterns extends AbstractRoute {
protected function get_route_post_response( WP_REST_Request $request ) {
$ptk_patterns_store = Package::container()->get( PTKPatternsStore::class );
- $ptk_patterns_store->fetch_patterns();
+ $patterns = $ptk_patterns_store->fetch_patterns();
+
+ $block_patterns = Package::container()->get( BlockPatterns::class );
+
+ $block_patterns->register_ptk_patterns( $patterns );
return rest_ensure_response(
array(
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/AbstractAddressSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/AbstractAddressSchema.php
index 3d77a9e2aaa..2ecf7ebc3e7 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/AbstractAddressSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/AbstractAddressSchema.php
@@ -172,6 +172,13 @@ abstract class AbstractAddressSchema extends AbstractSchema {
// correct format, and finally the second validation step is to ensure the correctly-formatted values
// match what we expect (postcode etc.).
foreach ( $address as $key => $value ) {
+
+ // Only run specific validation on properties that are defined in the schema and present in the address.
+ // This is for partial address pushes when only part of a customer address is sent.
+ // Full schema address validation still happens later, so empty, required values are disallowed.
+ if ( empty( $schema[ $key ] ) || empty( $address[ $key ] ) ) {
+ continue;
+ }
if ( is_wp_error( rest_validate_value_from_schema( $value, $schema[ $key ], $key ) ) ) {
$errors->add(
'invalid_' . $key,
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/CheckoutSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/CheckoutSchema.php
index d61c84d5af5..95a1694b1f8 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/CheckoutSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/CheckoutSchema.php
@@ -319,6 +319,9 @@ class CheckoutSchema extends AbstractSchema {
},
$field['options']
);
+ if ( true !== $field['required'] ) {
+ $field_schema['enum'][] = '';
+ }
}
if ( 'checkbox' === $field['type'] ) {
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/CartController.php b/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
index 5757c1649a4..a272cba8b9e 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
@@ -12,7 +12,6 @@ use Automattic\WooCommerce\StoreApi\Utilities\ArrayUtils;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
use Automattic\WooCommerce\StoreApi\Utilities\NoticeHandler;
use Automattic\WooCommerce\StoreApi\Utilities\QuantityLimits;
-use Automattic\WooCommerce\Blocks\Package;
use WP_Error;
/**
@@ -27,20 +26,40 @@ class CartController {
* Makes the cart and sessions available to a route by loading them from core.
*/
public function load_cart() {
- if ( ! did_action( 'woocommerce_load_cart_from_session' ) && function_exists( 'wc_load_cart' ) ) {
- wc_load_cart();
+ if ( did_action( 'woocommerce_load_cart_from_session' ) ) {
+ return;
}
+
+ // Initialize the cart.
+ wc_load_cart();
+
+ // Load cart from session.
+ $cart = $this->get_cart_instance();
+ $cart->get_cart();
}
/**
- * Recalculates the cart totals.
+ * Gets the latest cart instance, and ensures totals have been calculated before returning.
+ *
+ * @return \WC_Cart
+ */
+ public function get_cart_for_response() {
+ return did_action( 'woocommerce_after_calculate_totals' ) ? $this->get_cart_instance() : $this->calculate_totals();
+ }
+
+ /**
+ * Recalculates the cart totals and returns the updated cart instance.
+ *
+ * @since 9.2.0 Calculate shipping was removed here because it's called already by calculate_totals.
+ *
+ * @return \WC_Cart
*/
public function calculate_totals() {
$cart = $this->get_cart_instance();
$cart->get_cart();
$cart->calculate_fees();
- $cart->calculate_shipping();
$cart->calculate_totals();
+ return $cart;
}
/**
@@ -188,19 +207,19 @@ class CartController {
$cart_item = $this->get_cart_item( $item_id );
if ( empty( $cart_item ) ) {
- throw new RouteException( 'woocommerce_rest_cart_invalid_key', __( 'Cart item does not exist.', 'woocommerce' ), 409 );
+ throw new RouteException( 'woocommerce_rest_cart_invalid_key', esc_html__( 'Cart item does not exist.', 'woocommerce' ), 409 );
}
$product = $cart_item['data'];
if ( ! $product instanceof \WC_Product ) {
- throw new RouteException( 'woocommerce_rest_cart_invalid_product', __( 'Cart item is invalid.', 'woocommerce' ), 404 );
+ throw new RouteException( 'woocommerce_rest_cart_invalid_product', esc_html__( 'Cart item is invalid.', 'woocommerce' ), 404 );
}
$quantity_validation = ( new QuantityLimits() )->validate_cart_item_quantity( $quantity, $cart_item );
if ( is_wp_error( $quantity_validation ) ) {
- throw new RouteException( $quantity_validation->get_error_code(), $quantity_validation->get_error_message(), 400 );
+ throw new RouteException( $quantity_validation->get_error_code(), $quantity_validation->get_error_message(), 400 ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
$cart = $this->get_cart_instance();
@@ -725,7 +744,7 @@ class CartController {
$cart = wc()->cart;
if ( ! $cart || ! $cart instanceof \WC_Cart ) {
- throw new RouteException( 'woocommerce_rest_cart_error', __( 'Unable to retrieve cart.', 'woocommerce' ), 500 );
+ throw new RouteException( 'woocommerce_rest_cart_error', esc_html__( 'Unable to retrieve cart.', 'woocommerce' ), 500 );
}
return $cart;
@@ -824,7 +843,7 @@ class CartController {
// Add extra package data to array.
$packages = array_map(
- function( $key, $package, $index ) {
+ function ( $key, $package, $index ) {
$package['package_id'] = isset( $package['package_id'] ) ? $package['package_id'] : $key;
$package['package_name'] = isset( $package['package_name'] ) ? $package['package_name'] : $this->get_package_name( $package, $index );
return $package;
@@ -909,7 +928,7 @@ class CartController {
'woocommerce_rest_cart_coupon_error',
sprintf(
/* translators: %s coupon code */
- __( '"%s" is an invalid coupon code.', 'woocommerce' ),
+ esc_html__( '"%s" is an invalid coupon code.', 'woocommerce' ),
esc_html( $coupon_code )
),
400
@@ -921,7 +940,7 @@ class CartController {
'woocommerce_rest_cart_coupon_error',
sprintf(
/* translators: %s coupon code */
- __( 'Coupon code "%s" has already been applied.', 'woocommerce' ),
+ esc_html__( 'Coupon code "%s" has already been applied.', 'woocommerce' ),
esc_html( $coupon_code )
),
400
@@ -942,7 +961,7 @@ class CartController {
// Prevents new coupons being added if individual use coupons are already in the cart.
$individual_use_coupons = $this->get_cart_coupons(
- function( $code ) {
+ function ( $code ) {
$coupon = new \WC_Coupon( $code );
return $coupon->get_individual_use();
}
@@ -969,8 +988,8 @@ class CartController {
'woocommerce_rest_cart_coupon_error',
sprintf(
/* translators: %s: coupon code */
- __( '"%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ),
- $code
+ esc_html__( '"%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ),
+ esc_html( $code )
),
400
);
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php b/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php
index 505d7e10f21..71bc49f7d43 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php
@@ -142,7 +142,7 @@ trait CheckoutTrait {
* @param \WP_REST_Request $request Full details about the request.
*/
private function update_order_from_request( \WP_REST_Request $request ) {
- $this->order->set_customer_note( $request['customer_note'] ?? '' );
+ $this->order->set_customer_note( wc_sanitize_textarea( $request['customer_note'] ) ?? '' );
$this->order->set_payment_method( $this->get_request_payment_method_id( $request ) );
$this->order->set_payment_method_title( $this->get_request_payment_method_title( $request ) );
$this->persist_additional_fields_for_order( $request );
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php b/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php
index e7413b35b1c..4ef0342b28d 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php
@@ -100,7 +100,6 @@ class OrderController {
// Ensure cart is current.
if ( $update_totals ) {
- wc()->cart->calculate_shipping();
wc()->cart->calculate_totals();
}
diff --git a/plugins/woocommerce/src/StoreApi/docs/checkout.md b/plugins/woocommerce/src/StoreApi/docs/checkout.md
index ef09146e411..418bca01b29 100644
--- a/plugins/woocommerce/src/StoreApi/docs/checkout.md
+++ b/plugins/woocommerce/src/StoreApi/docs/checkout.md
@@ -88,6 +88,7 @@ POST /wc/store/v1/checkout
| `customer_note` | string | No | Note added to the order by the customer during checkout. |
| `payment_method` | string | Yes | The ID of the payment method being used to process the payment. |
| `payment_data` | array | No | Data to pass through to the payment method when processing payment. |
+| `customer_password`| string | No | Optionally define a password for new accounts. |
```sh
curl --header "Nonce: 12345" --request POST https://example-store.com/wp-json/wc/store/v1/checkout?payment_method=paypal&payment_data[0][key]=test-key&payment_data[0][value]=test-value
diff --git a/plugins/woocommerce/src/Utilities/OrderUtil.php b/plugins/woocommerce/src/Utilities/OrderUtil.php
index 8e94cbb8a9c..07525be6adb 100644
--- a/plugins/woocommerce/src/Utilities/OrderUtil.php
+++ b/plugins/woocommerce/src/Utilities/OrderUtil.php
@@ -228,4 +228,19 @@ final class OrderUtil {
return $count_per_status;
}
+ /**
+ * Removes the 'wc-' prefix from status.
+ *
+ * @param string $status The status to remove the prefix from.
+ *
+ * @return string The status without the prefix.
+ * @since 9.2.0
+ */
+ public static function remove_status_prefix( string $status ): string {
+ if ( strpos( $status, 'wc-' ) === 0 ) {
+ $status = substr( $status, 3 );
+ }
+
+ return $status;
+ }
}
diff --git a/plugins/woocommerce/src/Utilities/PluginUtil.php b/plugins/woocommerce/src/Utilities/PluginUtil.php
index 6bd063e86e3..e3c229a64fb 100644
--- a/plugins/woocommerce/src/Utilities/PluginUtil.php
+++ b/plugins/woocommerce/src/Utilities/PluginUtil.php
@@ -67,6 +67,34 @@ class PluginUtil {
require_once ABSPATH . WPINC . '/plugin.php';
}
+ /**
+ * Wrapper for WP's private `wp_get_active_and_valid_plugins` and `wp_get_active_network_plugins` functions.
+ *
+ * This combines the results of the two functions to get a list of all plugins that are active within a site.
+ * It's more useful than just retrieving the option values because it also validates that the plugin files exist.
+ * This wrapper is also a hedge against backward-incompatible changes since both of the WP methods are marked as
+ * being "@access private", so if need be we can update our methods here to preserve functionality.
+ *
+ * Note that the doc block for `wp_get_active_and_valid_plugins` says it returns "Array of paths to plugin files
+ * relative to the plugins directory", but it actually returns absolute paths.
+ *
+ * @return string[] Array of absolute paths to plugin files.
+ */
+ public function get_all_active_valid_plugins() {
+ $local = wp_get_active_and_valid_plugins();
+
+ if ( is_multisite() ) {
+ require_once ABSPATH . WPINC . '/ms-load.php';
+ $network = wp_get_active_network_plugins();
+ } else {
+ $network = array();
+ }
+
+ $all = array_merge( $local, $network );
+
+ return array_unique( $all );
+ }
+
/**
* Get a list with the names of the WordPress plugins that are WooCommerce aware
* (they have a "WC tested up to" header).
diff --git a/plugins/woocommerce/templates/cart/mini-cart.php b/plugins/woocommerce/templates/cart/mini-cart.php
index 34b3b021d3c..ecf51eb96eb 100644
--- a/plugins/woocommerce/templates/cart/mini-cart.php
+++ b/plugins/woocommerce/templates/cart/mini-cart.php
@@ -14,7 +14,7 @@
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
- * @version 7.9.0
+ * @version 9.2.0
*/
defined( 'ABSPATH' ) || exit;
@@ -47,13 +47,15 @@ do_action( 'woocommerce_before_mini_cart' ); ?>
echo apply_filters( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'woocommerce_cart_item_remove_link',
sprintf(
- '
× ',
+ '
× ',
esc_url( wc_get_cart_remove_url( $cart_item_key ) ),
/* translators: %s is the product name */
esc_attr( sprintf( __( 'Remove %s from cart', 'woocommerce' ), wp_strip_all_tags( $product_name ) ) ),
esc_attr( $product_id ),
esc_attr( $cart_item_key ),
- esc_attr( $_product->get_sku() )
+ esc_attr( $_product->get_sku() ),
+ /* translators: %s is the product name */
+ esc_attr( sprintf( __( '“%s” has been removed from your cart', 'woocommerce' ), wp_strip_all_tags( $product_name ) ) )
),
$cart_item_key
);
diff --git a/plugins/woocommerce/templates/emails/customer-reset-password.php b/plugins/woocommerce/templates/emails/customer-reset-password.php
index d2b62cfc10f..8549bcdfa79 100644
--- a/plugins/woocommerce/templates/emails/customer-reset-password.php
+++ b/plugins/woocommerce/templates/emails/customer-reset-password.php
@@ -12,7 +12,7 @@
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates\Emails
- * @version 4.0.0
+ * @version 9.3.0
*/
if ( ! defined( 'ABSPATH' ) ) {
@@ -31,7 +31,7 @@ if ( ! defined( 'ABSPATH' ) ) {
-
+
diff --git a/plugins/woocommerce/templates/emails/email-order-details.php b/plugins/woocommerce/templates/emails/email-order-details.php
index ece102e9dbc..fc689dd52c4 100644
--- a/plugins/woocommerce/templates/emails/email-order-details.php
+++ b/plugins/woocommerce/templates/emails/email-order-details.php
@@ -78,7 +78,7 @@ do_action( 'woocommerce_email_before_order_table', $order, $sent_to_admin, $plai
?>
- get_customer_note() ) ) ); ?>
+ get_customer_note() ) ), array() ); ?>
$reset_key, 'id' => $user_id ), wc_get_endpoint_url( 'lost-password', '', wc_get_page_permalink( 'myaccount' ) ) ) ) . "\n\n"; // phpcs:ignore
+echo esc_url( add_query_arg( array( 'key' => $reset_key, 'id' => $user_id, 'login' => rawurlencode( $user_login ) ), wc_get_endpoint_url( 'lost-password', '', wc_get_page_permalink( 'myaccount' ) ) ) ) . "\n\n"; // phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
echo "\n\n----------------------------------------\n\n";
diff --git a/plugins/woocommerce/templates/emails/plain/email-order-details.php b/plugins/woocommerce/templates/emails/plain/email-order-details.php
index ee52c8e0f41..1408057b776 100644
--- a/plugins/woocommerce/templates/emails/plain/email-order-details.php
+++ b/plugins/woocommerce/templates/emails/plain/email-order-details.php
@@ -43,7 +43,7 @@ if ( $item_totals ) {
}
if ( $order->get_customer_note() ) {
- echo esc_html__( 'Note:', 'woocommerce' ) . "\t " . wp_kses_post( wptexturize( $order->get_customer_note() ) ) . "\n";
+ echo esc_html__( 'Note:', 'woocommerce' ) . "\t " . wp_kses( wptexturize( $order->get_customer_note() ), array() ) . "\n";
}
if ( $sent_to_admin ) {
diff --git a/plugins/woocommerce/templates/loop/add-to-cart.php b/plugins/woocommerce/templates/loop/add-to-cart.php
index ad3f086e2a9..934ba5db90d 100644
--- a/plugins/woocommerce/templates/loop/add-to-cart.php
+++ b/plugins/woocommerce/templates/loop/add-to-cart.php
@@ -12,7 +12,7 @@
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
- * @version 9.0.0
+ * @version 9.2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
@@ -21,12 +21,14 @@ if ( ! defined( 'ABSPATH' ) ) {
global $product;
+$aria_describedby = isset( $args['aria-describedby_text'] ) ? sprintf( 'aria-describedby="woocommerce_loop_add_to_cart_link_describedby_%s"', esc_attr( $product->get_id() ) ) : '';
+
echo apply_filters(
'woocommerce_loop_add_to_cart_link', // WPCS: XSS ok.
sprintf(
- '
%s ',
+ '
%s ',
esc_url( $product->add_to_cart_url() ),
- esc_attr( $product->get_id() ),
+ $aria_describedby,
esc_attr( isset( $args['quantity'] ) ? $args['quantity'] : 1 ),
esc_attr( isset( $args['class'] ) ? $args['class'] : 'button' ),
isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '',
@@ -36,6 +38,8 @@ echo apply_filters(
$args
);
?>
-
-
-
+
+
+
+
+
diff --git a/plugins/woocommerce/templates/myaccount/my-address.php b/plugins/woocommerce/templates/myaccount/my-address.php
index 8871ee493a0..7afa63209b0 100644
--- a/plugins/woocommerce/templates/myaccount/my-address.php
+++ b/plugins/woocommerce/templates/myaccount/my-address.php
@@ -12,7 +12,7 @@
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
- * @version 8.7.0
+ * @version 9.2.0
*/
defined( 'ABSPATH' ) || exit;
@@ -60,7 +60,15 @@ $col = 1;
+
diff --git a/plugins/woocommerce/templates/parts/product-filters.html b/plugins/woocommerce/templates/parts/product-filters.html
new file mode 100644
index 00000000000..d2caafbe412
--- /dev/null
+++ b/plugins/woocommerce/templates/parts/product-filters.html
@@ -0,0 +1,91 @@
+
+
+
Filters
+
+
+
+
+
Active
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/woocommerce/tests/api-core-tests/.eslintrc.js b/plugins/woocommerce/tests/api-core-tests/.eslintrc.js
deleted file mode 100644
index 7cdd6ec4d3d..00000000000
--- a/plugins/woocommerce/tests/api-core-tests/.eslintrc.js
+++ /dev/null
@@ -1,13 +0,0 @@
-module.exports = {
- extends: [ 'plugin:@woocommerce/eslint-plugin/recommended' ],
- rules: {
- 'jsdoc/check-tag-names': 'off',
- 'jest/no-test-callback': 'off',
- camelcase: 'off',
- 'jest/no-disabled-tests': 'off',
- 'no-shadow': 'off',
- 'jest/no-identical-title': 'off',
- 'jest/no-standalone-expect': 'off',
- 'no-console': 'off',
- },
-};
diff --git a/plugins/woocommerce/tests/api-core-tests/README.md b/plugins/woocommerce/tests/api-core-tests/README.md
deleted file mode 100644
index b26220270f4..00000000000
--- a/plugins/woocommerce/tests/api-core-tests/README.md
+++ /dev/null
@@ -1,498 +0,0 @@
-# WooCommerce Core API Test Suite
-
-This package contains automated API tests for WooCommerce, based on Playwright and `wp-env`. It supersedes the SuperTest based [api-core-tests package](https://www.npmjs.com/package/@woocommerce/api-core-tests) and e2e-environment [setup](../tests/e2e), which we will gradually deprecate.
-
-## Table of contents
-
-- [Pre-requisites](#pre-requisites)
-- [Introduction](#introduction)
-- [About the Environment](#about-the-environment)
-- [Test Variables](#test-variables)
-- [Guide for writing API tests](#guide-for-writing-api-tests)
- - [What aspects of the API should we test?](#what-aspects-of-the-api-should-we-test)
- - [Creating test structure](#creating-test-structure)
- - [Test Data Setup/Teardown](#test-data-setupteardown)
- - [Writing the test - A Quick Start Guide](#writing-the-test---a-quick-start-guide)
- - [Examples](#examples)
- - [Debugging tests](#debugging-tests)
-- [Guide for using test reports](#guide-for-using-test-reports)
- - [Viewing the Playwright HTML report](#viewing-the-playwright-html-report)
- - [Viewing the Allure report](#viewing-the-allure-report)
-
-## Pre-requisites
-
-- Node.js ([Installation instructions](https://nodejs.org/en/download/))
-- NVM ([Installation instructions](https://github.com/nvm-sh/nvm))
-- PNPM ([Installation instructions](https://pnpm.io/installation))
-- Docker and Docker Compose ([Installation instructions](https://docs.docker.com/engine/install/))
-
-Note, that if you are on Mac and you install Docker through other methods such as homebrew, for example, your steps to set it up might be different. The commands listed in steps below may also vary.
-
-If you are using Windows, we recommend using [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/) for running tests. Follow the [WSL Setup Instructions](../tests/e2e/WSL_SETUP_INSTRUCTIONS.md) first before proceeding with the steps below.
-
-### Introduction
-
-WooCommerce's `api-core-tests` are powered by Playwright. The test site is spun up using `wp-env` (recommended), but we will continue to support `e2e-environment` in the meantime.
-
-**Running tests for the first time:**
-
-- `nvm use`
-- `pnpm install`
-- `pnpm --filter='@woocommerce/plugin-woocommerce' build`
-- `cd plugins/woocommerce`
-- `pnpm env:test`
-- `pnpm test:api-pw`
-
-To run the test again, re-create the environment to start with a fresh state
-
-- `pnpm env:destroy`
-- `pnpm env:test`
-- `pnpm test:api-pw`
-
-Other ways of running tests:
-
-- `pnpm test:api-pw ./tests/api-core-tests/tests/hello/hello.test.js` (running a single test file)
-- `pnpm test:api-pw ./tests/api-core-tests/tests/hello` (running all tests in a single folder)
-
-To see all options, run `cd plugins/woocommerce && pnpm playwright test --help`
-
-## Environment variables
-
-The following environment variables can be configured as shown in `.env.example`:
-
-```
-# Your site's base URL, not including a trailing slash
-API_BASE_URL="https://mysite.com"
-
-# The admin user's username or generated consumer key
-USER_KEY=""
-
-# The admin user's password or generated consumer secret
-USER_SECRET=""
-```
-
-For local setup, create a `.env` file in the `woocommerce/plugins/woocommerce/tests/api-core-tests` folder with the three required values described above. If any of these variables are configured they will override the values automatically set in the `playwright.config.js`
-
-When using a username and password combination instead of a consumer secret and consumer key, make sure to have the [JSON Basic Authentication plugin](https://github.com/WP-API/Basic-Auth) installed and activated on the test site.
-
-For more information about authentication with the WooCommerce API, please see the [Authentication](https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#authentication) section in the WooCommerce REST API documentation.
-
-### About the environment
-
-The default values are:
-
-- Latest stable WordPress version
-- PHP 7.4
-- MariaDB
-- URL: `http://localhost:8086/`
-- Admin credentials: `admin/password`
-
-If you want to customize these, check the [Test Variables](#test-variables) section.
-
-
-For more information how to configure the test environment for `wp-env`, please checkout the [documentation](https://github.com/WordPress/gutenberg/tree/trunk/packages/env) documentation.
-
-### Test Variables
-
-The test environment uses the following test variables:
-
-```json
-{
- "url": "http://localhost:8086/",
- "users": {
- "admin": {
- "username": "admin",
- "password": "password"
- },
- "customer": {
- "username": "customer",
- "password": "password"
- }
- }
-}
-```
-
-If you need to modify the port for your local test environment (eg. port is already in use) or use, edit [playwright.config.js](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/playwright.config.js). Depending on what environment tool you are using, you will need to also edit the respective `.json` file.
-
-**Modify the port wp-env**
-
-Edit [.wp-env.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/.wp-env.json) and [playwright.config.js](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/playwright.config.js).
-
-**Modify port for e2e-environment**
-
-Edit [tests/e2e/config/default.json](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/e2e/config/default.json).****
-
-### Starting/stopping the environment
-
-After you run a test, it's best to restart the environment to start from a fresh state. We are currently working to reset the state more efficiently to avoid the restart being needed, but this is a work-in-progress.
-
-- `pnpm env:down` to stop the environment
-- `pnpm env:destroy` when you make changes to `.wp-env.json`
-- `pnpm env:test` to spin up the test environment
-
-## Guide for writing API tests
-
-When writing new tests, a good source on how to get started is to reference the [existing tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests/tests). Data that is required for the tests should be located in an equivalent file in the [data](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests/data) folder.
-
-Good examples to reference are the `coupons` and `customers` tests. The [Quick Start Guide](#writing-the-test---a-quick-start-guide) below has the key steps to put together a test and examples of how those steps were implemented for `coupons` and `customers`.
-
-The [Playwright documentation](https://playwright.dev/docs/intro) is a good source for finding out more details on the various methods used in the tests, including what is available to you when you write new tests. The [API testing](https://playwright.dev/docs/test-api-testing) section has a good example on how the [playwright.config.js](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/api-core-tests/playwright.config.js) is used (see [Configuration](https://playwright.dev/docs/test-api-testing#configuration) section) and also how [Setup and Teardown](https://playwright.dev/docs/test-api-testing#setup-and-teardown) works.
-
-Note: Playwright uses the [expect](https://jestjs.io/docs/expect) library for test assertions. This library provides a lot of matchers like `toEqual`, `toContain`, `toHaveLength` and many more. Examples of these are throughout the test files.
-
-## What aspects of the API should we test?
-
-Assuming that we have validated the API contract (inspected the spec/contract, made sure the endpoints are correctly named, resources and types reflect the object model and there is no missing/duplicate functionality) we are ready to test.
-
-A test contains 3 different stages:
-1. `precondition`
-2. `action`
-3. `validation`
-
-and in the case of API testing, this equates to:
-1. `data creation` - see [Test Data Setup Examples](#test-data-setup-examples) below
-2. `send API request` - see [Request Examples](#request-examples) below
-3. `response validation` - see [Validation Examples](#validation-examples) below
-
-For each API request method (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`), the test would need to take the following actions:
-1. Verify correct HTTP status code. For example, creating a resource should return 201 CREATED and un-permitted requests should return 403 FORBIDDEN, etc.
-2. Verify response payload. Check the JSON body is valid and field names, types, and values are correct (i.e. check the response payload schema is implemented according to the specification) — including error responses.
-
-3. Verify response headers where appropriate e.g. Some headers hold information related to search totals, pagination values etc. (see [Examples](#examples) below).
-
-At a minimum, we want to ensure each possible CRUD operation can be applied and sufficient assertions have been validated based on the above.
-
-A reasonable process would be:
-1. Create an object using a `POST` request
-2. Retrieve the created object using the `GET` request
-3. Update the object using a `POST` request
-4. Delete the object using a `DELETE`request
-5. Test any batch create/update/delete operations (if applicable)
-
-Additional tests can also be added to test the following general test scenarios:
-
-- Basic positive tests (happy paths) - check basic functionality and the acceptance criteria of the API
-- Extended positive testing with optional parameters - more thorough testing (can be used for testing a bug/updates/new functionality)
-- Negative testing with valid input - expect to gracefully handle problem scenarios with valid user input e.g. trying to add an existing username
-- Negative testing with invalid input - expect to gracefully handle problem scenarios with invalid user input e.g. trying to add a username which is null
-
-The intention here is to validate that we get error responses when expected as per specification and the error status code and message are correct as per documentation.
-
-## Creating test structure
-
-The structure of the test serves as a skeleton for the test itself.
-
-Each test file requires the `@playwright/test` module to be imported as follows:
-```js
-const { test, expect } = require( '@playwright/test' );
-```
-You can create a test by using the `test.describe()` and `test()` methods of Playwright:
-
-- [`test.describe()`](https://playwright.dev/docs/api/class-test#test-describe-1) - creates a block that groups together several related tests;
-- [`test()`](https://playwright.dev/docs/api/class-test#test-call) - actual method that runs the test.
-
-Based on our example, the test skeleton would look as follows:
-
-```js
-test.describe( 'Coupons API tests', () => {
- test( 'can create a coupon', async ( {request} ) => {
- // test to create a coupon here
- } );
-
- test( 'can retrieve a coupon', async ( {request} ) => {
- // test to retrieve a coupon here
- } );
-
- test( 'can update a coupon', async ( { request } ) => {
- // test to update a coupon here
- } );
-} );
-```
-
-Note: you can also nest a `test.describe()` inside a `test.describe()`. Example:
-
-```js
-test.describe('Orders API tests: CRUD', () => {
- let orderId; //test variable
-
- test.describe('Create an order', () => {
- test('can create a pending order by default', async ({request}) => {
- //test code here
- }
-```
-This allows you to further subgroup tests. When viewing the tests results locally, each test describe 'level' will be separated by `>` in the console as below:
-
-`Orders API tests: CRUD › Create an order › can create a pending order by default`
-
-## Test Data Setup/Teardown
-
-You may need test data setup prior to the execution of your tests. If so, make sure it is removed after the execution of your tests. This can be achieved with any of the following methods, depending on the needs of the test:
-
-- [`test.beforeAll()`](https://playwright.dev/docs/api/class-test#test-before-all) - runs before all the tests in file/group
-- [`test.beforeEach()`](https://playwright.dev/docs/api/class-test#test-before-each) - runs before each test in file/group
-- [`test.afterEach()`](https://playwright.dev/docs/api/class-test#test-after-each) - runs after each test in file/group
-- [`test.afterAll()`](https://playwright.dev/docs/api/class-test#test-after-all) - runs after all the tests in file/group
-
-
-## Writing the test - a Quick Start Guide
-
-1. Ensure you have your authentication setup as mentioned in the [Environment Variables](#environment-variables) section above. i.e.
- > For local setup, create a `.env` file
-2. Create `test.js` file inside the tests directory
- - Example for [`coupons`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js) and [`customers`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js)
-3. Import `@playwright/test` module
- - Example for [`coupons`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L1) and [`customers`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js#L1)
-4. Group tests with `test.describe()` methods
- - Example for [`coupons`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L10) and [`customers`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js#L20)
-5. Add tests with `test()` methods
- - Example for [`coupons`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L14) and [`customers`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js#L93)
-6. Separate data where required into files in the `data` directory
- - Example for [`coupons`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/api-core-tests/data/coupon.js) and [`customers`](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/api-core-tests/data/customer.js)
-7. Import data required by your tests
- - Example for [`coupons`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L2) and [`customers`](https://github.com/woocommerce/woocommerce/blob/b904fd428db1252f39cb64005f4b627f2a9ac08e/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js#L5)
-8. After writing your tests, ensure all tests pass successfully with `pnpm test:api-pw`
-
-If you have made updates to functionality that breaks the tests then the tests should be updated accordingly. Similarly, if there is new functionality added then new tests should be added.
-
-## Examples
-
-Below are examples in our `api-core-tests` including references and typical API test operations.
-
-Playwright [configuration file](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/api-core-tests/playwright.config.js)
-
-Test files [location](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests/tests)
-
-Data files [location](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests/data)
-
-### Test Data Setup Examples
-
-Setup data with [test.beforeAll()](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L347)
-```js
- test.beforeAll( async ( { request } ) => {
- // Create a coupon
- const createCouponResponse = await request.post(
- '/wp-json/wc/v3/coupons/',
- {
- data: testCoupon,
- }
- );
- const createCouponResponseJSON = await createCouponResponse.json();
- testCoupon.id = createCouponResponseJSON.id;
- } );
- ```
-
-Teardown data with [test.afterAll()](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L360)
-```js
- // Clean up created coupon and order
- test.afterAll( async ( { request } ) => {
- await request.delete( `/wp-json/wc/v3/coupons/${ testCoupon.id }`, {
- data: { force: true },
- } );
- await request.delete( `/wp-json/wc/v3/orders/${ orderId }`, {
- data: { force: true },
- } );
- } );
- ```
-### Request Examples
-
-`GET` [request](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L44)
-```js
- //call API to get previously created coupon
- const response = await request.get(
- `/wp-json/wc/v3/coupons/${ couponId }`
- );
-```
-
-`POST` [request](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L63)
-```js
- //call API to update previously created coupon
- const response = await request.post(
- `/wp-json/wc/v3/coupons/${ couponId }`,
- {
- data: updatedCouponDetails,
- }
- );
-```
-
-`PUT` [request](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/orders/order-complex.test.js#L220)
-```js
- //ensure tax calculations are enabled
- await request.put(
- '/wp-json/wc/v3/settings/general/woocommerce_calc_taxes',
- {
- data: {
- value: 'yes',
- },
- }
- );
-```
-
-`DELETE` [request](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L80)
-```js
-//call API to delete previously created coupon
- const response = await request.delete(
- `/wp-json/wc/v3/coupons/${ couponId }`,
- {
- data: { force: true },
- }
- );
-```
-
-`BATCH` [request](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L119)
-```js
- // Batch create 2 new coupons.
- const batchCreatePayload = {
- create: expectedCoupons,
- };
-
- // call API to batch create coupons
- const batchCreateResponse = await request.post(
- 'wp-json/wc/v3/coupons/batch',
- {
- data: batchCreatePayload,
- }
- );
-```
-
-### Validation Examples
-
-Verify [Status code](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L30)
-```js
- expect( response.status() ).toEqual( 201 );
-```
-
-Verify [Response payload](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L33)
-```js
-//validate the response data
- expect( await response.json() ).toEqual(
- expect.objectContaining( {
- code: testCoupon.code,
- amount: Number( coupon.amount ).toFixed( 2 ),
- discount_type: coupon.discount_type,
- } )
- );
-```
-
-Verify [field names](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L142)
-```js
- expect( id ).toBeDefined();
- expect( code ).toEqual( expectedCouponCode );
-```
-
-Verify [field types](https://github.com/woocommerce/woocommerce/blob/d19c20491e5a7ade64c8fd530f01e0f3f3f7e29c/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js#L85)
-```js
-expect(typeof responseJSON.id).toEqual('string');
-```
-
-Verify [field values](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L186)
-```js
-expect( updatedCoupons[ 1 ].amount ).toEqual( '25.00' );
-```
-
-Verify [field length](https://github.com/woocommerce/woocommerce/blob/4ac1d822ac9082102536b0f7aa9cb0553965adaa/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L136)
-```js
-expect( actualCoupons ).toHaveLength( expectedCoupons.length );
-```
-
-Verify search [headers](https://github.com/woocommerce/woocommerce/blob/d19c20491e5a7ade64c8fd530f01e0f3f3f7e29c/plugins/woocommerce/tests/api-core-tests/tests/orders/orders.test.js#L2703)
-```js
-// Verify total page count.
- expect( page1.headers()[ 'x-wp-total' ] ).toEqual(
- ORDERS_COUNT.toString()
- );
- expect( page1.headers()[ 'x-wp-totalpages' ] ).toEqual( '3' );
- ```
-
-Verify variable [not undefined](https://github.com/woocommerce/woocommerce/blob/778cb130f27d0dd0dc7da1acb0e89762f81c0f18/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L31)
-```js
-expect( couponId ).toBeDefined();
-```
-
-Verify [response contains an object](https://github.com/woocommerce/woocommerce/blob/778cb130f27d0dd0dc7da1acb0e89762f81c0f18/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L73)
-```js
-expect( await response.json() ).toEqual(
- expect.objectContaining( updatedCouponDetails )
- );
-```
-
-Verify [response contains an array containing an object](https://github.com/woocommerce/woocommerce/blob/778cb130f27d0dd0dc7da1acb0e89762f81c0f18/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js#L388)
-```js
- expect( responseJSON.coupon_lines[ 0 ].meta_data ).toEqual(
- expect.arrayContaining( [
- expect.objectContaining( {
- key: 'coupon_data',
- value: expect.objectContaining( {
- code: testCoupon.code,
- } ),
- } ),
- ] )
- );
-```
-
-## Debugging tests
-
-The Playwright debugger won't work for the API tests as it is based around GUI interactions.
-
-For now it is simple enough to add `console.log()` statements to output the values of your response/JSON/variables/status etc. Be sure to remove them when done ;)
-
-You can also use the handy [REST API Log](https://wordpress.org/plugins/wp-rest-api-log/) plugin to see the API request information within WordPress. It displays the details, request headers, query params, body params, body content, response headers and response body information.
-
-For the list of WooCommerce API endpoints, expected responses, and more, please see the [WooCommerce REST API Documentation](https://woocommerce.github.io/woocommerce-rest-api-docs/).
-
-## Guide for using test reports
-
-The tests would generate three kinds of reports after the run:
-1. A Playwright HTML report.
-1. A Playwright JSON report.
-1. Allure results.
-
-By default, they are saved inside the `test-results` folder. If you want to save them in a custom location, just assign the absolute path to the environment variables mentioned in the [Playwright](https://playwright.dev/docs/test-reporters) and [Allure-Playwright](https://www.npmjs.com/package/allure-playwright) documentation.
-
-| Report | Default location | Environment variable for custom location |
-| ----------- | ---------------- | ---------------------------------------- |
-| Playwright HTML report | `test-results/playwright-report` | `PLAYWRIGHT_HTML_REPORT` |
-| Playwright JSON report | `test-results/test-results.json` | `PLAYWRIGHT_JSON_OUTPUT_NAME` |
-| Allure results | `test-results/allure-results` | `ALLURE_RESULTS_DIR` |
-
-### Viewing the Playwright HTML report
-
-Use the `playwright show-report $PATH_TO_PLAYWRIGHT_HTML_REPORT` command to open the report. For example, assuming that you're at the root of the WooCommerce monorepo, and that you did not specify a custom location for the report, you would use the following commands:
-
-```bash
-cd plugins/woocommerce
-pnpm exec playwright show-report tests/api-core-tests/test-results/playwright-report
-```
-
-For more details about the Playwright HTML report, see their [HTML Reporter](https://playwright.dev/docs/test-reporters#html-reporter) documentation.
-
-### Viewing the Allure report
-
-This assumes that you're already familiar with reports generated by the [Allure Framework](https://github.com/allure-framework), particularly:
-- What the `allure-results` and `allure-report` folders are, and how they're different from each other.
-- Allure commands like `allure generate` and `allure open`.
-
-Use the `allure generate` command to generate an HTML report from the `allure-results` directory created at the end of the test run. Then, use the `allure open` command to open it on your browser. For example, assuming that:
-- You're at the root of the WooCommerce monorepo
-- You did not specify a custom location for `allure-results` (you did not assign a value to `ALLURE_RESULTS_DIR`)
-- You want to generate the `allure-report` folder in `plugins/woocommerce/tests/api-core-tests/test-results`
-
-Then you would need to use the following commands:
-
-```bash
-cd plugins/woocommerce
-pnpm exec allure generate --clean tests/api-core-tests/test-results/allure-results --output tests/api-core-tests/test-results/allure-report
-pnpm exec allure open tests/api-core-tests/test-results/allure-report
-```
-
-A browser window should open the Allure report.
-
-If you're using [WSL](https://learn.microsoft.com/en-us/windows/wsl/about) however, you might get this message right after running the `allure open` command:
-```
-Starting web server...
-2022-12-09 18:52:01.323:INFO::main: Logging initialized @286ms to org.eclipse.jetty.util.log.StdErrLog
-Can not open browser because this capability is not supported on your platform. You can use the link below to open the report manually.
-Server started at
. Press
to exit
-```
-In this case, take note of the port number (38917 in the example above) and then use it to navigate to `http://localhost`. Taking the example above, you should be able to view the Allure report on http://localhost:38917.
-
-To know more about the allure-playwright integration, see their [GitHub documentation](https://github.com/allure-framework/allure-js/tree/master/packages/allure-playwright).
diff --git a/plugins/woocommerce/tests/api-core-tests/ci-release.global-setup.js b/plugins/woocommerce/tests/api-core-tests/ci-release.global-setup.js
deleted file mode 100644
index 80689772d13..00000000000
--- a/plugins/woocommerce/tests/api-core-tests/ci-release.global-setup.js
+++ /dev/null
@@ -1,122 +0,0 @@
-const { UPDATE_WC, USER_KEY, USER_SECRET } = process.env;
-const { test: setup, expect } = require( '@playwright/test' );
-const fs = require( 'fs' );
-const { downloadWooCommerceRelease } = require( './utils/plugin-utils' );
-const pluginEndpoint = '/wp-json/wp/v2/plugins/woocommerce/woocommerce';
-
-let zipPath;
-
-setup( `Setup remote test site`, async ( { page, request } ) => {
- setup.setTimeout( 5 * 60 * 1000 );
-
- await setup.step( `Download WooCommerce build zip`, async () => {
- zipPath = await downloadWooCommerceRelease( { request } );
- } );
-
- await setup.step( 'Login to wp-admin', async () => {
- const Username = 'Username or Email Address';
- const Password = 'Password';
- const Log_In = 'Log In';
- const Dashboard = 'Dashboard';
-
- // Need to wait until network idle. Otherwise, Password field gets auto-cleared after typing password in.
- await page.goto( '/wp-admin', { waitUntil: 'networkidle' } );
- await page.getByLabel( Username ).fill( USER_KEY );
- await page.getByLabel( Password, { exact: true } ).fill( USER_SECRET );
- await page.getByRole( 'button', { name: Log_In } ).click();
- await expect(
- page
- .locator( '#menu-dashboard' )
- .getByRole( 'link', { name: Dashboard } )
- ).toBeVisible();
- } );
-
- const installed = await setup.step(
- `See if there's a WooCommerce plugin installed`,
- async () => {
- const response = await request.get( pluginEndpoint );
- const isOK = response.ok();
- const status = response.status();
-
- // Fast-fail if response was neither OK nor 404.
- expect( isOK || status === 404 ).toEqual( true );
-
- return isOK;
- }
- );
-
- await setup.step(
- `Deactivate currently installed WooCommerce version`,
- async () => {
- if ( ! installed ) {
- return;
- }
-
- const options = {
- data: {
- status: 'inactive',
- },
- };
- const response = await request.put( pluginEndpoint, options );
- expect( response.ok() ).toBeTruthy();
- }
- );
-
- await setup.step(
- `Delete currently installed WooCommerce version`,
- async () => {
- if ( ! installed ) {
- return;
- }
-
- const response = await request.delete( pluginEndpoint );
- expect( response.ok() ).toBeTruthy();
- }
- );
-
- await setup.step( `Install WooCommerce ${ UPDATE_WC }`, async () => {
- const Upload_Plugin = 'Upload Plugin';
- const Plugin_zip_file = 'Plugin zip file';
- const Install_Now = 'Install Now';
- const Activate_Plugin = 'Activate Plugin';
- const timeout = 3 * 60 * 1000;
-
- await page.goto( '/wp-admin/plugin-install.php' );
- await page.getByRole( 'button', { name: Upload_Plugin } ).click();
- await page.getByLabel( Plugin_zip_file ).setInputFiles( zipPath );
- await page.getByRole( 'button', { name: Install_Now } ).click();
- await expect(
- page.getByRole( 'link', { name: Activate_Plugin } )
- ).toBeVisible( { timeout } );
- } );
-
- await setup.step( `Activate WooCommerce`, async () => {
- const options = {
- data: {
- status: 'active',
- },
- };
- const response = await request.put( pluginEndpoint, options );
- expect( response.ok() ).toBeTruthy();
- } );
-
- await setup.step( `Verify WooCommerce version was installed`, async () => {
- const response = await request.get( pluginEndpoint );
- const { status, version } = await response.json();
- expect( status ).toEqual( 'active' );
- expect( version ).toEqual( UPDATE_WC );
- } );
-
- await setup.step( `Verify WooCommerce database version`, async () => {
- const response = await request.get( '/wp-json/wc/v3/system_status' );
- const { database } = await response.json();
- const { wc_database_version } = database;
- const [ major, minor ] = UPDATE_WC.split( '.' );
- const pattern = new RegExp( `^${ major }\.${ minor }` );
- expect( wc_database_version ).toMatch( pattern );
- } );
-
- await setup.step( `Delete zip`, async () => {
- fs.unlinkSync( zipPath );
- } );
-} );
diff --git a/plugins/woocommerce/tests/api-core-tests/ci-release.playwright.config.js b/plugins/woocommerce/tests/api-core-tests/ci-release.playwright.config.js
deleted file mode 100644
index e44208c2fc2..00000000000
--- a/plugins/woocommerce/tests/api-core-tests/ci-release.playwright.config.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const defaultConfig = require( './playwright.config' );
-const { devices } = require( '@playwright/test' );
-
-// Global setup will be done through the 'Setup' project, not through the `globalSetup` property
-delete defaultConfig[ 'globalSetup' ];
-
-/**
- * @type {import('@playwright/test').PlaywrightTestConfig}
- */
-const config = {
- ...defaultConfig,
- projects: [
- {
- name: 'Setup',
- testDir: './',
- testMatch: 'ci-release.global-setup.js',
- use: { ...devices[ 'Desktop Chrome' ] },
- },
- {
- name: 'API tests',
- dependencies: [ 'Setup' ],
- },
- ],
-};
-
-module.exports = config;
diff --git a/plugins/woocommerce/tests/api-core-tests/data/customer.js b/plugins/woocommerce/tests/api-core-tests/data/customer.js
deleted file mode 100644
index 0b4f50f5e80..00000000000
--- a/plugins/woocommerce/tests/api-core-tests/data/customer.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * This file contains objects that can be used as test data for scenarios around creating, retrieivng, updating, and deleting customers.
- *
- * For more details on the Product properties, see:
- *
- * https://woocommerce.github.io/woocommerce-rest-api-docs/#customers
- *
- */
-
-/**
- * A customer
- */
- const customer = {
- email: "john.doe@example.com",
- first_name: "John",
- last_name: "Doe",
- username: "john.doe",
- billing: {
- first_name: "John",
- last_name: "Doe",
- company: "",
- address_1: "969 Market",
- address_2: "",
- city: "San Francisco",
- state: "CA",
- postcode: "94103",
- country: "US",
- email: "john.doe@example.com",
- phone: "(555) 555-5555"
- },
- shipping: {
- first_name: "John",
- last_name: "Doe",
- company: "",
- address_1: "969 Market",
- address_2: "",
- city: "San Francisco",
- state: "CA",
- postcode: "94103",
- country: "US"
- }
- };
-
- module.exports = {
- customer,
-};
\ No newline at end of file
diff --git a/plugins/woocommerce/tests/api-core-tests/data/tax-rate.js b/plugins/woocommerce/tests/api-core-tests/data/tax-rate.js
deleted file mode 100644
index 024b749a918..00000000000
--- a/plugins/woocommerce/tests/api-core-tests/data/tax-rate.js
+++ /dev/null
@@ -1,421 +0,0 @@
-/**
- * A standard tax rate.
- *
- * For more details on the tax rate properties, see:
- *
- * https://woocommerce.github.io/woocommerce-rest-api-docs/#tax-rate-properties
- *
- */
-const standardTaxRate = {
- name: 'Standard Rate',
- rate: '10.0000',
- class: 'standard',
-};
-
-const reducedTaxRate = {
- name: 'Reduced Rate',
- rate: '1.0000',
- class: 'reduced-rate',
-};
-
-const zeroTaxRate = {
- name: 'Zero Rate',
- rate: '0.0000',
- class: 'zero-rate',
-};
-
-const getTaxRateExamples = () => {
- return { standardTaxRate, reducedTaxRate, zeroTaxRate };
-};
-
-const allUSTaxesExample = [
- {
- country: "US",
- state: "AL",
- rate: "4.0000",
- name: "State Tax",
- shipping: false,
- order: 1
- },
- {
- country: "US",
- state: "AZ",
- rate: "5.6000",
- name: "State Tax",
- shipping: false,
- order: 2
- },
- {
- country: "US",
- state: "AR",
- rate: "6.5000",
- name: "State Tax",
- shipping: true,
- order: 3
- },
- {
- country: "US",
- state: "CA",
- rate: "7.5000",
- name: "State Tax",
- shipping: false,
- order: 4
- },
- {
- country: "US",
- state: "CO",
- rate: "2.9000",
- name: "State Tax",
- shipping: false,
- order: 5
- },
- {
- country: "US",
- state: "CT",
- rate: "6.3500",
- name: "State Tax",
- shipping: true,
- order: 6
- },
- {
- country: "US",
- state: "DC",
- rate: "5.7500",
- name: "State Tax",
- shipping: true,
- order: 7
- },
- {
- country: "US",
- state: "FL",
- rate: "6.0000",
- name: "State Tax",
- shipping: true,
- order: 8
- },
- {
- country: "US",
- state: "GA",
- rate: "4.0000",
- name: "State Tax",
- shipping: true,
- order: 9
- },
- {
- country: "US",
- state: "GU",
- rate: "4.0000",
- name: "State Tax",
- shipping: false,
- order: 10
- },
- {
- country: "US",
- state: "HI",
- rate: "4.0000",
- name: "State Tax",
- shipping: true,
- order: 11
- },
- {
- country: "US",
- state: "ID",
- rate: "6.0000",
- name: "State Tax",
- shipping: false,
- order: 12
- },
- {
- country: "US",
- state: "IL",
- rate: "6.2500",
- name: "State Tax",
- shipping: false,
- order: 13
- },
- {
- country: "US",
- state: "IN",
- rate: "7.0000",
- name: "State Tax",
- shipping: false,
- order: 14
- },
- {
- country: "US",
- state: "IA",
- rate: "6.0000",
- name: "State Tax",
- shipping: false,
- order: 15
- },
- {
- country: "US",
- state: "KS",
- rate: "6.1500",
- name: "State Tax",
- shipping: true,
- order: 16
- },
- {
- country: "US",
- state: "KY",
- rate: "6.0000",
- name: "State Tax",
- shipping: true,
- order: 17
- },
- {
- country: "US",
- state: "LA",
- rate: "4.0000",
- name: "State Tax",
- shipping: false,
- order: 18
- },
- {
- country: "US",
- state: "ME",
- rate: "5.5000",
- name: "State Tax",
- shipping: false,
- order: 19
- },
- {
- country: "US",
- state: "MD",
- rate: "6.0000",
- name: "State Tax",
- shipping: false,
- order: 20
- },
- {
- country: "US",
- state: "MA",
- rate: "6.2500",
- name: "State Tax",
- shipping: false,
- order: 21
- },
- {
- country: "US",
- state: "MI",
- rate: "6.0000",
- name: "State Tax",
- shipping: true,
- order: 22
- },
- {
- country: "US",
- state: "MN",
- rate: "6.8750",
- name: "State Tax",
- shipping: true,
- order: 23
- },
- {
- country: "US",
- state: "MS",
- rate: "7.0000",
- name: "State Tax",
- shipping: true,
- order: 24
- },
- {
- country: "US",
- state: "MO",
- rate: "4.2250",
- name: "State Tax",
- shipping: false,
- order: 25
- },
- {
- country: "US",
- state: "NE",
- rate: "5.5000",
- name: "State Tax",
- shipping: true,
- order: 26
- },
- {
- country: "US",
- state: "NV",
- rate: "6.8500",
- name: "State Tax",
- shipping: false,
- order: 27
- },
- {
- country: "US",
- state: "NJ",
- rate: "7.0000",
- name: "State Tax",
- shipping: true,
- order: 28
- },
- {
- country: "US",
- state: "NM",
- rate: "5.1250",
- name: "State Tax",
- shipping: true,
- order: 29
- },
- {
- country: "US",
- state: "NY",
- rate: "4.0000",
- name: "State Tax",
- shipping: true,
- order: 30
- },
- {
- country: "US",
- state: "NC",
- rate: "4.7500",
- name: "State Tax",
- shipping: true,
- order: 31
- },
- {
- country: "US",
- state: "ND",
- rate: "5.0000",
- name: "State Tax",
- shipping: true,
- order: 32
- },
- {
- country: "US",
- state: "OH",
- rate: "5.7500",
- name: "State Tax",
- shipping: true,
- order: 33
- },
- {
- country: "US",
- state: "OK",
- rate: "4.5000",
- name: "State Tax",
- shipping: false,
- order: 34
- },
- {
- country: "US",
- state: "PA",
- rate: "6.0000",
- name: "State Tax",
- shipping: true,
- order: 35
- },
- {
- country: "US",
- state: "PR",
- rate: "6.0000",
- name: "State Tax",
- shipping: false,
- order: 36
- },
- {
- country: "US",
- state: "RI",
- rate: "7.0000",
- name: "State Tax",
- shipping: false,
- order: 37
- },
- {
- country: "US",
- state: "SC",
- rate: "6.0000",
- name: "State Tax",
- shipping: true,
- order: 38
- },
- {
- country: "US",
- state: "SD",
- rate: "4.0000",
- name: "State Tax",
- shipping: true,
- order: 39
- },
- {
- country: "US",
- state: "TN",
- rate: "7.0000",
- name: "State Tax",
- shipping: true,
- order: 40
- },
- {
- country: "US",
- state: "TX",
- rate: "6.2500",
- name: "State Tax",
- shipping: true,
- order: 41
- },
- {
- country: "US",
- state: "UT",
- rate: "5.9500",
- name: "State Tax",
- shipping: false,
- order: 42
- },
- {
- country: "US",
- state: "VT",
- rate: "6.0000",
- name: "State Tax",
- shipping: true,
- order: 43
- },
- {
- country: "US",
- state: "VA",
- rate: "5.3000",
- name: "State Tax",
- shipping: false,
- order: 44
- },
- {
- country: "US",
- state: "WA",
- rate: "6.5000",
- name: "State Tax",
- shipping: true,
- order: 45
- },
- {
- country: "US",
- state: "WV",
- rate: "6.0000",
- name: "State Tax",
- shipping: true,
- order: 46
- },
- {
- country: "US",
- state: "WI",
- rate: "5.0000",
- name: "State Tax",
- shipping: true,
- order: 47
- },
- {
- country: "US",
- state: "WY",
- rate: "4.0000",
- name: "State Tax",
- shipping: true,
- order: 48
- }
- ];
-
-module.exports = {
- getTaxRateExamples,
- allUSTaxesExample
-};
diff --git a/plugins/woocommerce/tests/api-core-tests/global-setup.js b/plugins/woocommerce/tests/api-core-tests/global-setup.js
deleted file mode 100644
index 33c8c545027..00000000000
--- a/plugins/woocommerce/tests/api-core-tests/global-setup.js
+++ /dev/null
@@ -1,251 +0,0 @@
-const { DISABLE_HPOS, GITHUB_TOKEN, UPDATE_WC } = process.env;
-const { downloadZip, deleteZip } = require( './utils/plugin-utils' );
-const axios = require( 'axios' ).default;
-const playwrightConfig = require( './playwright.config' );
-const { site } = require( './utils' );
-
-/**
- *
- * @param {import('@playwright/test').FullConfig} config
- */
-module.exports = async ( config ) => {
- // If API_BASE_URL is configured and doesn't include localhost, running on daily host
- if (
- process.env.API_BASE_URL &&
- ! process.env.API_BASE_URL.includes( 'localhost' )
- ) {
- const { chromium, expect } = require( '@playwright/test' );
-
- const { baseURL, userAgent } = config.projects[ 0 ].use;
- const contextOptions = { baseURL, userAgent };
-
- const browser = await chromium.launch();
- const setupContext = await browser.newContext( contextOptions );
- const setupPage = await setupContext.newPage();
-
- const getWCDownloadURL = async () => {
- const requestConfig = {
- method: 'get',
- url: 'https://api.github.com/repos/woocommerce/woocommerce/releases',
- headers: {
- Accept: 'application/vnd.github+json',
- },
- params: {
- per_page: 100,
- },
- };
- if ( GITHUB_TOKEN ) {
- requestConfig.headers.Authorization = `Bearer ${ GITHUB_TOKEN }`;
- }
- const response = await axios( requestConfig ).catch( ( error ) => {
- if ( error.response ) {
- console.log( error.response.data );
- }
- throw new Error( error.message );
- } );
- const releaseWithTagName = response.data.find(
- ( { tag_name } ) => tag_name === UPDATE_WC
- );
- if ( ! releaseWithTagName ) {
- throw new Error(
- `No release with tag_name="${ UPDATE_WC }" found. If "${ UPDATE_WC }" is a draft release, make sure to specify a GITHUB_TOKEN environment variable.`
- );
- }
- const wcZipAsset = releaseWithTagName.assets.find( ( { name } ) =>
- name.match( /^woocommerce(-trunk-nightly)?\.zip$/ )
- );
- if ( wcZipAsset ) {
- return GITHUB_TOKEN
- ? wcZipAsset.url
- : wcZipAsset.browser_download_url;
- }
- throw new Error(
- `WooCommerce release with tag "${ UPDATE_WC }" found, but does not have a WooCommerce ZIP asset.`
- );
- };
-
- const url = await getWCDownloadURL();
- const params = { url };
-
- if ( GITHUB_TOKEN ) {
- params.authorizationToken = GITHUB_TOKEN;
- }
-
- const woocommerceZipPath = await downloadZip( params );
-
- let adminLoggedIn = false;
- let pluginActive = false;
-
- console.log( '--------------------------------------' );
- console.log( 'Running daily tests, resetting site...' );
- console.log( '--------------------------------------' );
-
- const adminRetries = 5;
- for ( let i = 0; i < adminRetries; i++ ) {
- try {
- console.log( 'Trying to log-in as admin...' );
- await setupPage.goto( '/wp-admin' );
- await setupPage
- .locator( 'input[name="log"]' )
- .fill( process.env.USER_KEY );
- await setupPage
- .locator( 'input[name="pwd"]' )
- .fill( process.env.USER_SECRET );
- await setupPage.locator( 'text=Log In' ).click();
-
- await expect( setupPage.locator( 'div.wrap > h1' ) ).toHaveText(
- 'Dashboard'
- );
- console.log( 'Logged-in as admin successfully.' );
- adminLoggedIn = true;
- break;
- } catch ( e ) {
- console.log(
- `Admin log-in failed, Retrying... ${ i }/${ adminRetries }`
- );
- console.log( e );
- }
- }
-
- if ( ! adminLoggedIn ) {
- console.error(
- 'Cannot proceed api test, as admin login failed. Please check if the test site has been setup correctly.'
- );
- process.exit( 1 );
- }
-
- await setupPage.goto( 'wp-admin/plugins.php' );
- await expect( setupPage.locator( 'div.wrap > h1' ) ).toHaveText(
- 'Plugins'
- );
-
- console.log( 'Deactivating WooCommerce Plugin...' );
- await setupPage.locator( '#deactivate-woocommerce' ).click();
- await expect( setupPage.locator( 'div#message' ) ).toHaveText(
- 'Plugin deactivated.Dismiss this notice.'
- );
-
- console.log( 'Deleting WooCommerce Plugin...' );
- setupPage.on( 'dialog', ( dialog ) => dialog.accept() );
- await setupPage.locator( '#delete-woocommerce' ).click();
- await expect( setupPage.locator( '#woocommerce-deleted' ) ).toHaveText(
- 'WooCommerce was successfully deleted.'
- );
-
- for ( let i = 0; i < adminRetries; i++ ) {
- try {
- console.log( 'Reinstalling WooCommerce Plugin...' );
- await setupPage.goto( 'wp-admin/plugin-install.php' );
- await setupPage.locator( 'a.upload-view-toggle' ).click();
- await expect(
- setupPage.locator( 'p.install-help' )
- ).toBeVisible();
- await expect(
- setupPage.locator( 'p.install-help' )
- ).toContainText(
- 'If you have a plugin in a .zip format, you may install or update it by uploading it here'
- );
- const [ fileChooser ] = await Promise.all( [
- setupPage.waitForEvent( 'filechooser' ),
- setupPage.locator( '#pluginzip' ).click(),
- ] );
- await fileChooser.setFiles( woocommerceZipPath );
- console.log( 'Uploading nightly build...' );
- await setupPage
- .locator( '#install-plugin-submit' )
- .click( { timeout: 60000 } );
- await setupPage.waitForLoadState( 'networkidle', {
- timeout: 60000,
- } );
- await expect(
- setupPage.getByRole(
- 'link',
- { name: 'Activate Plugin' },
- { timeout: 60000 }
- )
- ).toBeVisible();
- console.log( 'Activating Plugin...' );
- await setupPage
- .getByRole( 'link', { name: 'Activate Plugin' } )
- .click( { timeout: 60000 } );
- pluginActive = true;
- break;
- } catch ( e ) {
- console.log(
- `Installing and activating plugin failed, Retrying... ${ i }/${ adminRetries }`
- );
- console.log( e );
- }
- }
- if ( ! pluginActive ) {
- console.error(
- 'Cannot proceed api test, as installing WC failed. Please check if the test site has been setup correctly.'
- );
- process.exit( 1 );
- }
-
- console.log( 'WooCommerce Re-installed.' );
- await expect(
- setupPage.getByRole( 'heading', { name: 'Welcome to Woo!' } )
- ).toBeVisible();
-
- await deleteZip( woocommerceZipPath );
-
- // Might need to update the database
- await setupPage.goto( 'wp-admin/plugins.php' );
- const updateButton = setupPage.locator(
- 'text=Update WooCommerce Database'
- );
- const updateCompleteMessage = setupPage.locator(
- 'text=WooCommerce database update complete.'
- );
- await expect( setupPage.locator( 'div.wrap > h1' ) ).toHaveText(
- 'Plugins'
- );
- if ( await updateButton.isVisible() ) {
- console.log( 'Database update button present. Click it.' );
- await updateButton.click( { timeout: 60000 } );
- await expect( updateCompleteMessage ).toBeVisible();
- } else {
- console.log( 'No DB update needed' );
- }
- } else {
- // running on localhost using wp-env so ensure HPOS is set if DISABLE_HPOS env variable is passed
- if ( DISABLE_HPOS ) {
- let hposConfigured = false;
- const value = DISABLE_HPOS === '1' ? 'no' : 'yes';
- try {
- const auth = {
- username: playwrightConfig.userKey,
- password: playwrightConfig.userSecret,
- };
- const hposResponse = await axios.post(
- playwrightConfig.use.baseURL +
- '/wp-json/wc/v3/settings/advanced/woocommerce_custom_orders_table_enabled',
- { value },
- { auth }
- );
- if ( hposResponse.data.value === value ) {
- console.log(
- `HPOS Switched ${
- value === 'yes' ? 'on' : 'off'
- } successfully`
- );
- hposConfigured = true;
- }
- } catch ( error ) {
- console.log( 'HPOS setup failed.' );
- console.log( error );
- process.exit( 1 );
- }
- if ( ! hposConfigured ) {
- console.error(
- 'Cannot proceed to api tests, HPOS configuration failed. Please check if the correct DISABLE_HPOS value was used and the test site has been setup correctly.'
- );
- process.exit( 1 );
- }
- }
-
- await site.useCartCheckoutShortcodes( config );
- }
-};
diff --git a/plugins/woocommerce/tests/api-core-tests/playwright.config.js b/plugins/woocommerce/tests/api-core-tests/playwright.config.js
deleted file mode 100644
index 6a3c9207974..00000000000
--- a/plugins/woocommerce/tests/api-core-tests/playwright.config.js
+++ /dev/null
@@ -1,71 +0,0 @@
-const { devices } = require( '@playwright/test' );
-require( 'dotenv' ).config( { path: __dirname + '/.env' } );
-
-const { API_BASE_URL, CI, DEFAULT_TIMEOUT_OVERRIDE, USER_KEY, USER_SECRET } =
- process.env;
-
-const baseURL = API_BASE_URL ?? 'http://localhost:8086';
-const userKey = USER_KEY ?? 'admin';
-const userSecret = USER_SECRET ?? 'password';
-const base64auth = btoa( `${ userKey }:${ userSecret }` );
-
-const config = {
- userKey,
- userSecret,
- timeout: DEFAULT_TIMEOUT_OVERRIDE
- ? Number( DEFAULT_TIMEOUT_OVERRIDE )
- : 90 * 1000,
- expect: { timeout: 20 * 1000 },
- globalSetup: require.resolve( './global-setup' ),
- outputDir: './test-results/report',
- testDir: 'tests',
- retries: CI ? 4 : 2,
- workers: 4,
- reporter: [
- [ 'list' ],
- [
- 'html',
- {
- outputFolder:
- process.env.PLAYWRIGHT_HTML_REPORT ??
- './test-results/playwright-report',
- open: CI ? 'never' : 'always',
- },
- ],
- [
- 'allure-playwright',
- {
- outputFolder:
- process.env.ALLURE_RESULTS_DIR ??
- './tests/api-core-tests/test-results/allure-results',
- },
- ],
- [
- 'json',
- {
- outputFile:
- process.env.PLAYWRIGHT_JSON_OUTPUT_NAME ??
- './test-results/test-results.json',
- },
- ],
- ],
- use: {
- screenshot: 'only-on-failure',
- video: 'on-first-retry',
- trace: 'retain-on-failure',
- viewport: { width: 1280, height: 720 },
- baseURL,
- extraHTTPHeaders: {
- // Add authorization token to all requests.
- Authorization: `Basic ${ base64auth }`,
- },
- },
- projects: [
- {
- name: 'Chrome',
- use: { ...devices[ 'Desktop Chrome' ] },
- },
- ],
-};
-
-module.exports = config;
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js
deleted file mode 100644
index 2766df5af22..00000000000
--- a/plugins/woocommerce/tests/api-core-tests/tests/reports/reports-crud.test.js
+++ /dev/null
@@ -1,407 +0,0 @@
-const {
- test,
- expect
-} = require('@playwright/test');
-
-/**
- * Tests for the WooCommerce Refunds API.
- *
- * @group api
- * @group reports
- *
- */
-test.describe('Reports API tests', () => {
-
- test('can view all reports', async ({
- request
- }) => {
- // call API to retrieve the reports
- const response = await request.get('/wp-json/wc/v3/reports');
- const responseJSON = await response.json();
- expect(response.status()).toEqual(200);
- expect(Array.isArray(responseJSON)).toBe(true);
-
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "sales",
- "description": "List of sales reports.",
- })
- ]));
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "sales",
- "description": "List of sales reports.",
- })
- ]));
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "sales",
- "description": "List of sales reports.",
- })
- ]));
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "sales",
- "description": "List of sales reports.",
- })
- ]));
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "sales",
- "description": "List of sales reports.",
- })
- ]));
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "sales",
- "description": "List of sales reports.",
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "sales",
- "description": "List of sales reports.",
- })
- ]));
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "sales",
- "description": "List of sales reports.",
- })
- ]));
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "sales",
- "description": "List of sales reports.",
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "sales",
- "description": "List of sales reports.",
- })
- ]));
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "sales",
- "description": "List of sales reports.",
- })
- ]));
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "sales",
- "description": "List of sales reports.",
- })
- ]));
- });
-
- test('can view sales reports', async ({
- request
- }) => {
- // call API to retrieve the sales reports
- const response = await request.get('/wp-json/wc/v3/reports/sales');
- const responseJSON = await response.json();
- expect(response.status()).toEqual(200);
- expect(Array.isArray(responseJSON)).toBe(true);
-
- const today = new Date();
- const dd = String(today.getDate()).padStart(2, '0');
- const mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0!
- const yyyy = today.getFullYear();
- const dateString = yyyy + '-' + mm + '-' + dd;
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "total_sales": expect.any(String),
- "net_sales": expect.any(String),
- "average_sales": expect.any(String),
- "total_orders": expect.any(Number),
- "total_items": expect.any(Number),
- "total_tax": expect.any(String),
- "total_shipping": expect.any(String),
- "total_refunds": expect.any(Number),
- "total_discount": expect.any(String),
- "totals_grouped_by": "day",
- "totals": expect.objectContaining({
- [dateString]: {
- "sales": expect.any(String),
- "orders": expect.any(Number),
- "items": expect.any(Number),
- "tax": expect.any(String),
- "shipping": expect.any(String),
- "discount": expect.any(String),
- "customers": expect.any(Number)
- }
- }),
- "total_customers": expect.any(Number),
- })
- ]));
- });
-
- test('can view top sellers reports', async ({
- request
- }) => {
- // call API to retrieve the top sellers
- const response = await request.get('/wp-json/wc/v3/reports/top_sellers');
- const responseJSON = await response.json();
- expect(response.status()).toEqual(200);
- expect(Array.isArray(responseJSON)).toBe(true);
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([]));
- });
-
- test('can view coupons totals', async ({
- request
- }) => {
- // call API to retrieve the coupons totals
- const response = await request.get('/wp-json/wc/v3/reports/coupons/totals');
- const responseJSON = await response.json();
- expect(response.status()).toEqual(200);
- expect(Array.isArray(responseJSON)).toBe(true);
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "percent",
- "name": "Percentage discount",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "fixed_cart",
- "name": "Fixed cart discount",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "fixed_product",
- "name": "Fixed product discount",
- "total": expect.any(Number)
- })
- ]));
- });
-
- test('can view customers totals', async ({
- request
- }) => {
- // call API to retrieve the customers totals
- const response = await request.get('/wp-json/wc/v3/reports/customers/totals');
- const responseJSON = await response.json();
- expect(response.status()).toEqual(200);
- expect(Array.isArray(responseJSON)).toBe(true);
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "paying",
- "name": "Paying customer",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "non_paying",
- "name": "Non-paying customer",
- "total": expect.any(Number)
- })
- ]));
- });
-
- test('can view orders totals', async ({
- request
- }) => {
- // call API to retrieve the orders totals
- const response = await request.get('/wp-json/wc/v3/reports/orders/totals');
- const responseJSON = await response.json();
- expect(response.status()).toEqual(200);
- expect(Array.isArray(responseJSON)).toBe(true);
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "pending",
- "name": "Pending payment",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "processing",
- "name": "Processing",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "on-hold",
- "name": "On hold",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "completed",
- "name": "Completed",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "cancelled",
- "name": "Cancelled",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "refunded",
- "name": "Refunded",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "failed",
- "name": "Failed",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "checkout-draft",
- "name": "Draft",
- "total": expect.any(Number)
- })
- ]));
- });
-
- test('can view products totals', async ({
- request
- }) => {
- // call API to retrieve the products totals
- const response = await request.get('/wp-json/wc/v3/reports/products/totals');
- const responseJSON = await response.json();
- expect(response.status()).toEqual(200);
- expect(Array.isArray(responseJSON)).toBe(true);
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "external",
- "name": "External/Affiliate product",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "grouped",
- "name": "Grouped product",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "simple",
- "name": "Simple product",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "variable",
- "name": "Variable product",
- "total": expect.any(Number)
- })
- ]));
- });
-
- test('can view reviews totals', async ({
- request
- }) => {
- // call API to retrieve the reviews totals
- const response = await request.get('/wp-json/wc/v3/reports/reviews/totals');
- const responseJSON = await response.json();
- expect(response.status()).toEqual(200);
- expect(Array.isArray(responseJSON)).toBe(true);
-
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "rated_1_out_of_5",
- "name": "Rated 1 out of 5",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "rated_2_out_of_5",
- "name": "Rated 2 out of 5",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "rated_3_out_of_5",
- "name": "Rated 3 out of 5",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "rated_4_out_of_5",
- "name": "Rated 4 out of 5",
- "total": expect.any(Number)
- })
- ]));
- expect(responseJSON).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- "slug": "rated_5_out_of_5",
- "name": "Rated 5 out of 5",
- "total": expect.any(Number)
- })
- ]));
- });
-});
diff --git a/plugins/woocommerce/tests/api-core-tests/utils/api.js b/plugins/woocommerce/tests/api-core-tests/utils/api.js
deleted file mode 100644
index f318657969c..00000000000
--- a/plugins/woocommerce/tests/api-core-tests/utils/api.js
+++ /dev/null
@@ -1,431 +0,0 @@
-const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default;
-const { async } = require( 'regenerator-runtime' );
-const config = require( '../playwright.config' );
-
-let api;
-
-// Ensure that global-setup.js runs before creating api client
-if ( process.env.CONSUMER_KEY && process.env.CONSUMER_SECRET ) {
- api = new wcApi( {
- url: config.use.baseURL,
- consumerKey: process.env.CONSUMER_KEY,
- consumerSecret: process.env.CONSUMER_SECRET,
- version: 'wc/v3',
- } );
-}
-
-/**
- * Allow explicit construction of api client.
- */
-const constructWith = ( consumerKey, consumerSecret ) => {
- api = new wcApi( {
- url: config.use.baseURL,
- consumerKey,
- consumerSecret,
- version: 'wc/v3',
- } );
-};
-
-const throwCustomError = (
- error,
- customMessage = 'Something went wrong. See details below.'
-) => {
- throw new Error(
- customMessage
- .concat(
- `\nResponse status: ${ error.response.status } ${ error.response.statusText }`
- )
- .concat(
- `\nResponse headers:\n${ JSON.stringify(
- error.response.headers,
- null,
- 2
- ) }`
- ).concat( `\nResponse data:\n${ JSON.stringify(
- error.response.data,
- null,
- 2
- ) }
-` )
- );
-};
-
-const update = {
- storeDetails: async ( store ) => {
- const res = await api.post( 'settings/general/batch', {
- update: [
- {
- id: 'woocommerce_store_address',
- value: store.address,
- },
- {
- id: 'woocommerce_store_city',
- value: store.city,
- },
- {
- id: 'woocommerce_default_country',
- value: store.countryCode,
- },
- {
- id: 'woocommerce_store_postcode',
- value: store.zip,
- },
- ],
- } );
- },
- enableCashOnDelivery: async () => {
- await api.put( 'payment_gateways/cod', {
- enabled: true,
- } );
- },
- disableCashOnDelivery: async () => {
- await api.put( 'payment_gateways/cod', {
- enabled: false,
- } );
- },
-};
-
-const get = {
- coupons: async ( params ) => {
- const response = await api
- .get( 'coupons', params )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when trying to list all coupons.'
- );
- } );
-
- return response.data;
- },
- defaultCountry: async () => {
- const response = await api.get(
- 'settings/general/woocommerce_default_country'
- );
-
- const code = response.data.default;
-
- return code;
- },
- orders: async ( params ) => {
- const response = await api
- .get( 'orders', params )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when trying to list all orders.'
- );
- } );
-
- return response.data;
- },
- products: async ( params ) => {
- const response = await api
- .get( 'products', params )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when trying to list all products.'
- );
- } );
-
- return response.data;
- },
- productAttributes: async ( params ) => {
- const response = await api
- .get( 'products/attributes', params )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when trying to list all product attributes.'
- );
- } );
-
- return response.data;
- },
- productCategories: async ( params ) => {
- const response = await api
- .get( 'products/categories', params )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when trying to list all product categories.'
- );
- } );
-
- return response.data;
- },
- productTags: async ( params ) => {
- const response = await api
- .get( 'products/tags', params )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when trying to list all product tags.'
- );
- } );
-
- return response.data;
- },
- shippingClasses: async ( params ) => {
- const response = await api
- .get( 'products/shipping_classes', params )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when trying to list all shipping classes.'
- );
- } );
-
- return response.data;
- },
-
- shippingZones: async ( params ) => {
- const response = await api
- .get( 'shipping/zones', params )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when trying to list all shipping zones.'
- );
- } );
-
- return response.data;
- },
- shippingZoneMethods: async ( shippingZoneId ) => {
- const response = await api
- .get( `shipping/zones/${ shippingZoneId }/methods` )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- `Something went wrong when trying to list all shipping methods in shipping zone ${ shippingZoneId }.`
- );
- } );
-
- return response.data;
- },
- taxClasses: async () => {
- const response = await api
- .get( 'taxes/classes' )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when trying to list all tax classes.'
- );
- } );
-
- return response.data;
- },
- taxRates: async ( params ) => {
- const response = await api
- .get( 'taxes', params )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when trying to list all tax rates.'
- );
- } );
-
- return response.data;
- },
-};
-
-const create = {
- product: async ( product ) => {
- const response = await api.post( 'products', product );
-
- return response.data.id;
- },
- /**
- * Batch create product variations.
- *
- * @see {@link [Batch update product variations](https://woocommerce.github.io/woocommerce-rest-api-docs/#batch-update-product-variations)}
- * @param {number|string} productId Product ID to add variations to
- * @param {object[]} variations Array of variations to add. See [Product variation properties](https://woocommerce.github.io/woocommerce-rest-api-docs/#product-variation-properties)
- * @returns {Promise} Array of variation ID's.
- */
- productVariations: async ( productId, variations ) => {
- const response = await api.post(
- `products/${ productId }/variations/batch`,
- {
- create: variations,
- }
- );
-
- return response.data.create.map( ( { id } ) => id );
- },
-};
-
-const deletePost = {
- coupons: async ( ids ) => {
- const res = await api
- .post( 'coupons/batch', { delete: ids } )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when batch deleting coupons.'
- );
- } );
-
- return res.data;
- },
- product: async ( id ) => {
- await api.delete( `products/${ id }`, {
- force: true,
- } );
- },
- products: async ( ids ) => {
- const res = await api
- .post( 'products/batch', { delete: ids } )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when batch deleting products.'
- );
- } );
- return res.data;
- },
- productAttributes: async ( id ) => {
- const res = await api
- .post( 'products/attributes/batch', { delete: id } )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when batch deleting product attributes.'
- );
- } );
- return res.data;
- },
- productCategories: async ( ids ) => {
- const res = await api
- .post( 'products/categories/batch', { delete: ids } )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when batch deleting product categories.'
- );
- } );
- return res.data;
- },
- productTags: async ( ids ) => {
- const res = await api
- .post( 'products/tags/batch', { delete: ids } )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when batch deleting product tags.'
- );
- } );
- return res.data;
- },
- order: async ( id ) => {
- await api.delete( `orders/${ id }`, {
- force: true,
- } );
- },
- orders: async ( ids ) => {
- const res = await api
- .post( 'orders/batch', { delete: ids } )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when batch deleting orders.'
- );
- } );
- return res.data;
- },
- shippingClasses: async ( ids ) => {
- const res = await api
- .post( 'products/shipping_classes/batch', { delete: ids } )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when batch deleting shipping classes.'
- );
- } );
- return res.data;
- },
- shippingZone: async ( id ) => {
- const res = await api
- .delete( `shipping/zones/${ id }`, {
- force: true,
- } )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when deleting shipping zone.'
- );
- } );
- return res.data;
- },
- shippingZoneMethod: async ( shippingZoneId, shippingMethodId ) => {
- const res = await api
- .delete(
- `shipping/zones/${ shippingZoneId }/methods/${ shippingMethodId }`,
- {
- force: true,
- }
- )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when deleting shipping zone method.'
- );
- } );
- return res.data;
- },
- taxClass: async ( slug ) => {
- const res = await api
- .delete( `taxes/classes/${ slug }`, {
- force: true,
- } )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- `Something went wrong when deleting tax class ${ slug }.`
- );
- } );
- return res.data;
- },
- taxRates: async ( ids ) => {
- const res = await api
- .post( 'taxes/batch', { delete: ids } )
- .then( ( response ) => response )
- .catch( ( error ) => {
- throwCustomError(
- error,
- 'Something went wrong when batch deleting tax rates.'
- );
- } );
- return res.data;
- },
-};
-
-module.exports = {
- update,
- get,
- create,
- deletePost,
- constructWith,
-};
diff --git a/plugins/woocommerce/tests/api-core-tests/utils/index.js b/plugins/woocommerce/tests/api-core-tests/utils/index.js
deleted file mode 100644
index 6f630c49a0e..00000000000
--- a/plugins/woocommerce/tests/api-core-tests/utils/index.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const api = require( './api' );
-const site = require( './site' );
-module.exports = {
- api,
- site,
-};
diff --git a/plugins/woocommerce/tests/api-core-tests/utils/plugin-utils.js b/plugins/woocommerce/tests/api-core-tests/utils/plugin-utils.js
deleted file mode 100644
index c147e2565b0..00000000000
--- a/plugins/woocommerce/tests/api-core-tests/utils/plugin-utils.js
+++ /dev/null
@@ -1,366 +0,0 @@
-const { APIRequest, expect } = require( '@playwright/test' );
-const axios = require( 'axios' ).default;
-const fs = require( 'fs' );
-const path = require( 'path' );
-const { promisify } = require( 'util' );
-const execAsync = promisify( require( 'child_process' ).exec );
-
-/**
- * GitHub [release asset](https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28) object.
- * @typedef {Object} ReleaseAsset
- * @property {string} name
- * @property {string} url
- */
-
-/**
- * GitHub [release](https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28) object.
- * @typedef {Object} Release
- * @property {ReleaseAsset[]} assets
- * @property {string} tag_name
- * @property {string} name
- */
-
-/**
- * Encode basic auth username and password to be used in HTTP Authorization header.
- *
- * @param {string} username
- * @param {string} password
- * @returns Base64-encoded string
- */
-const encodeCredentials = ( username, password ) => {
- return Buffer.from( `${ username }:${ password }` ).toString( 'base64' );
-};
-
-/**
- * Deactivate and delete a plugin specified by the given `slug` using the WordPress API.
- *
- * @param {object} params
- * @param {APIRequest} params.request
- * @param {string} params.baseURL
- * @param {string} params.slug
- * @param {string} params.username
- * @param {string} params.password
- */
-export const deletePlugin = async ( {
- request,
- baseURL,
- slug,
- username,
- password,
-} ) => {
- // Check if plugin is installed by getting the list of installed plugins, and then finding the one whose `textdomain` property equals `slug`.
- const apiContext = await request.newContext( {
- baseURL,
- extraHTTPHeaders: {
- Authorization: `Basic ${ encodeCredentials( username, password ) }`,
- cookie: '',
- },
- } );
- const listPluginsResponse = await apiContext.get(
- `/wp-json/wp/v2/plugins`,
- {
- failOnStatusCode: true,
- }
- );
- const pluginsList = await listPluginsResponse.json();
- const pluginToDelete = pluginsList.find(
- ( { textdomain } ) => textdomain === slug
- );
-
- // If installed, get its `plugin` value and use it to deactivate and delete it.
- if ( pluginToDelete ) {
- const { plugin } = pluginToDelete;
- const requestURL = `/wp-json/wp/v2/plugins/${ plugin }`;
-
- await apiContext.put( requestURL, {
- data: { status: 'inactive' },
- } );
-
- await apiContext.delete( requestURL );
- }
-};
-
-/**
- * Download the zip file from a remote location.
- *
- * @param {object} param
- * @param {string} param.url
- * @param {string} param.repository
- * @param {string} param.authorizationToken
- * @param {boolean} param.prerelease
- * @param {string} param.downloadDir
- *
- * @param {string} url The URL where the zip file is located. Takes precedence over `repository`.
- * @param {string} repository The repository owner and name. For example: `woocommerce/woocommerce`. Ignored when `url` was given.
- * @param {string} authorizationToken Authorization token used to authenticate with the GitHub API if required.
- * @param {boolean} prerelease Flag on whether to get a prelease or not. Default `false`.
- * @param {string} downloadDir Relative path to the download directory. Non-existing folders will be auto-created. Defaults to `tmp` under current working directory.
- *
- * @return {string} Absolute path to the downloaded zip.
- */
-export const downloadZip = async ( {
- url,
- repository,
- authorizationToken,
- prerelease = false,
- downloadDir = 'tmp',
-} ) => {
- let zipFilename = path.basename( url || repository );
- zipFilename = zipFilename.endsWith( '.zip' )
- ? zipFilename
- : zipFilename.concat( '.zip' );
- const zipFilePath = path.resolve( downloadDir, zipFilename );
-
- let response;
-
- // Create destination folder.
- fs.mkdirSync( downloadDir, { recursive: true } );
-
- const downloadURL =
- url ??
- ( await getLatestReleaseZipUrl( {
- repository,
- authorizationToken,
- prerelease,
- } ) );
-
- // Download the zip.
- const options = {
- method: 'get',
- url: downloadURL,
- responseType: 'stream',
- headers: {
- Authorization: authorizationToken
- ? `token ${ authorizationToken }`
- : '',
- Accept: 'application/octet-stream',
- },
- };
-
- response = await axios( options ).catch( ( error ) => {
- if ( error.response ) {
- console.error( error.response.data );
- }
- throw new Error( error.message );
- } );
-
- response.data.pipe( fs.createWriteStream( zipFilePath ) );
-
- return zipFilePath;
-};
-
-/**
- * Delete a zip file. Useful when cleaning up downloaded plugin zips.
- *
- * @param {string} zipFilePath Local file path to the ZIP.
- */
-export const deleteZip = async ( zipFilePath ) => {
- await fs.unlink( zipFilePath, ( err ) => {
- if ( err ) throw err;
- } );
-};
-
-/**
- * Get the download URL of the latest release zip for a plugin using GitHub API.
- *
- * @param {{repository: string, authorizationToken: string, prerelease: boolean, perPage: number}} param
- * @param {string} repository The repository owner and name. For example: `woocommerce/woocommerce`.
- * @param {string} authorizationToken Authorization token used to authenticate with the GitHub API if required.
- * @param {boolean} prerelease Flag on whether to get a prelease or not.
- * @param {number} perPage Limit of entries returned from the latest releases list, defaults to 3.
- * @return {string} Download URL for the release zip file.
- */
-export const getLatestReleaseZipUrl = async ( {
- repository,
- authorizationToken,
- prerelease = false,
- perPage = 3,
-} ) => {
- let release;
-
- const requesturl = prerelease
- ? `https://api.github.com/repos/${ repository }/releases?per_page=${ perPage }`
- : `https://api.github.com/repos/${ repository }/releases/latest`;
-
- const options = {
- method: 'get',
- url: requesturl,
- headers: {
- Authorization: authorizationToken
- ? `token ${ authorizationToken }`
- : '',
- },
- };
-
- // Get the first prerelease, or the latest release.
- let response;
- try {
- response = await axios( options );
- } catch ( error ) {
- let errorMessage =
- 'Something went wrong when downloading the plugin.\n';
-
- if ( error.response ) {
- // The request was made and the server responded with a status code
- // that falls out of the range of 2xx
- errorMessage = errorMessage.concat(
- `Response status: ${ error.response.status } ${ error.response.statusText }`,
- '\n',
- `Response body:`,
- '\n',
- JSON.stringify( error.response.data, null, 2 ),
- '\n'
- );
- } else if ( error.request ) {
- // The request was made but no response was received
- // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
- // http.ClientRequest in node.js
- errorMessage = errorMessage.concat(
- JSON.stringify( error.request, null, 2 ),
- '\n'
- );
- } else {
- // Something happened in setting up the request that triggered an Error
- errorMessage = errorMessage.concat( error.toJSON(), '\n' );
- }
-
- throw new Error( errorMessage );
- }
-
- release = prerelease
- ? response.data.find( ( { prerelease } ) => prerelease )
- : response.data;
-
- // If response contains assets, return URL of first asset.
- // Otherwise, return the github.com URL from the tag name.
- const { assets } = release;
- if ( assets && assets.length ) {
- return assets[ 0 ].url;
- } else {
- const tagName = release.tag_name;
- return `https://github.com/${ repository }/archive/${ tagName }.zip`;
- }
-};
-
-/**
- * Install a plugin using WP CLI within a WP ENV environment.
- * This is a workaround to the "The uploaded file exceeds the upload_max_filesize directive in php.ini" error encountered when uploading a plugin to the local WP Env E2E environment through the UI.
- *
- * @see https://github.com/WordPress/gutenberg/issues/29430
- *
- * @param {string} pluginPath
- */
-export const installPluginThruWpCli = async ( pluginPath ) => {
- const runWpCliCommand = async ( command ) => {
- const { stdout, stderr } = await execAsync(
- `pnpm exec wp-env run tests-cli -- ${ command }`
- );
-
- console.log( stdout );
- console.error( stderr );
- };
-
- const wpEnvPluginPath = pluginPath.replace(
- /.*\/plugins\/woocommerce/,
- 'wp-content/plugins/woocommerce'
- );
-
- await runWpCliCommand( `ls ${ wpEnvPluginPath }` );
-
- await runWpCliCommand(
- `wp plugin install --activate --force ${ wpEnvPluginPath }`
- );
-
- await runWpCliCommand( `wp plugin list` );
-};
-
-/**
- * Download the WooCommerce release zip. Can download draft releases when `token` is specified.
- *
- * @param {Object} params
- * @param {import("@playwright/test").APIRequestContext} params.request
- * @param {string} params.version The version indicated in the release `tag_name` or `name` field.
- * @param {string} params.token
- * @param {string} params.downloadDir
- *
- * @throws When `version` was not found.
- *
- * @returns {Promise} Absolute path to the downloaded WooCommerce zip.
- */
-export const downloadWooCommerceRelease = async ( {
- request,
- version = process.env.UPDATE_WC,
- token = process.env.GITHUB_TOKEN,
- downloadDir = 'tmp',
-} ) => {
- /**
- *
- * @returns {Promise}
- */
- const getRelease = async () => {
- const url =
- 'https://api.github.com/repos/woocommerce/woocommerce/releases';
- const options = {
- params: {
- per_page: 100,
- },
- headers: {
- Authorization: token ? `Bearer ${ token }` : undefined,
- },
- };
- const response = await request.get( url, options );
-
- /**
- * @type {Release[]}
- */
- const releases = await response.json();
-
- const match = releases.find( ( { tag_name, name } ) =>
- [ tag_name, name ].includes( version )
- );
-
- if ( ! match ) {
- throw new Error( `Release ${ version } not found!` );
- }
-
- return match;
- };
-
- /**
- *
- * @param {Release} release
- * @throws When `release` does not contain a woocommerce zip.
- * @returns {ReleaseAsset}
- */
- const getWooCommerceZipAsset = ( release ) => {
- const zipName =
- version.toLowerCase() === 'nightly'
- ? 'woocommerce-trunk-nightly.zip'
- : 'woocommerce.zip';
- const asset = release.assets.find( ( { name } ) => name === zipName );
-
- if ( ! asset ) {
- throw new Error(
- `Release ${ version } does not contain a WooCommerce ZIP asset`
- );
- }
-
- return asset;
- };
-
- const release = await getRelease();
- const asset = getWooCommerceZipAsset( release );
- const downloadResponse = await request.get( asset.url, {
- headers: {
- Authorization: token ? `Bearer ${ token }` : undefined,
- Accept: 'application/octet-stream',
- },
- } );
- expect( downloadResponse.ok() ).toBeTruthy();
- const body = await downloadResponse.body();
- const zipPath = path.resolve( downloadDir, asset.name );
- fs.mkdirSync( path.resolve( downloadDir ), { recursive: true } );
- fs.writeFileSync( zipPath, body );
-
- return zipPath;
-};
diff --git a/plugins/woocommerce/tests/api-core-tests/utils/site.js b/plugins/woocommerce/tests/api-core-tests/utils/site.js
deleted file mode 100644
index 1211c101b2a..00000000000
--- a/plugins/woocommerce/tests/api-core-tests/utils/site.js
+++ /dev/null
@@ -1,304 +0,0 @@
-const api = require( './api' );
-
-const deleteAllCoupons = async () => {
- console.log( 'Deleting all coupons...' );
-
- let coupons,
- page = 1;
-
- while (
- ( coupons = await api.get.coupons( { per_page: 100, page: page++ } ) )
- .length > 0
- ) {
- const ids = coupons.map( ( { id } ) => id );
- await api.deletePost.coupons( ids );
- }
-
- console.log( 'Done.' );
-};
-
-const deleteAllProducts = async () => {
- console.log( 'Deleting all products...' );
-
- let products,
- page = 1;
-
- while (
- ( products = await api.get.products( { per_page: 100, page: page++ } ) )
- .length > 0
- ) {
- const ids = products.map( ( { id } ) => id );
- await api.deletePost.products( ids );
- }
-
- console.log( 'Done.' );
-};
-
-const deleteAllProductAttributes = async () => {
- console.log( 'Deleting all product attributes...' );
-
- let attributes,
- page = 1;
-
- while (
- ( attributes = await api.get.productAttributes( {
- per_page: 100,
- page: page++,
- } ) ).length > 0
- ) {
- const ids = attributes.map( ( { id } ) => id );
- await api.deletePost.productAttributes( ids );
- }
-
- console.log( 'Done.' );
-};
-
-const deleteAllProductCategories = async () => {
- console.log( 'Deleting all product categories...' );
-
- let categories,
- page = 1;
-
- // Exclude "Uncategorized" as it cannot be deleted
- while (
- ( categories = (
- await api.get.productCategories( { per_page: 100, page: page++ } )
- ).filter( ( { slug } ) => slug !== 'uncategorized' ) ).length > 0
- ) {
- const ids = categories.map( ( { id } ) => id );
- await api.deletePost.productCategories( ids );
- }
-
- console.log( 'Done.' );
-};
-
-const deleteAllProductTags = async () => {
- console.log( 'Deleting all product tags...' );
-
- let tags,
- page = 1;
-
- while (
- ( tags = await api.get.productTags( {
- per_page: 100,
- page: page++,
- } ) ).length > 0
- ) {
- const ids = tags.map( ( { id } ) => id );
- await api.deletePost.productTags( ids );
- }
-
- console.log( 'Done.' );
-};
-
-const deleteAllOrders = async () => {
- console.log( 'Deleting all orders...' );
-
- let orders,
- page = 1;
-
- while (
- ( orders = await api.get.orders( { per_page: 100, page: page++ } ) )
- .length > 0
- ) {
- const ids = orders.map( ( { id } ) => id );
- await api.deletePost.orders( ids );
- }
-
- console.log( 'Done.' );
-};
-
-const deleteAllShippingZones = async () => {
- console.log( 'Deleting all shipping zones...' );
-
- let shippingZones,
- page = 1;
-
- // Exclude "Locations not covered by your other zones" as it cannot be deleted.
- while (
- ( shippingZones = (
- await api.get.shippingZones( {
- per_page: 100,
- page: page++,
- } )
- ).filter(
- ( { name } ) => name !== 'Locations not covered by your other zones'
- ) ).length > 0
- ) {
- const ids = shippingZones.map( ( { id } ) => id );
- for ( const id of ids ) {
- await api.deletePost.shippingZone( id );
- }
- }
-
- console.log( 'Done.' );
-};
-
-const deleteAllShippingClasses = async () => {
- console.log( 'Deleting all shipping classes...' );
-
- let shippingClasses,
- page = 1;
-
- while (
- ( shippingClasses = await api.get.shippingClasses( {
- per_page: 100,
- page: page++,
- } ) ).length > 0
- ) {
- const ids = shippingClasses.map( ( { id } ) => id );
- await api.deletePost.shippingClasses( ids );
- }
-
- console.log( 'Done.' );
-};
-
-const deleteAllShippingMethodsInDefaultShippingZone = async () => {
- console.log( 'Deleting all shipping methods...' );
-
- let shippingMethods;
-
- while (
- ( shippingMethods = await api.get.shippingZoneMethods( 0 ) ).length > 0
- ) {
- const ids = shippingMethods.map( ( { id } ) => id );
- for ( const id of ids ) {
- await api.deletePost.shippingZoneMethod( 0, id );
- }
- }
-
- console.log( 'Done.' );
-};
-
-const deleteAllTaxClasses = async () => {
- console.log( 'Deleting all non-default tax classes...' );
-
- let taxClasses;
-
- const getExistingNonDefaultTaxClasses = async () => {
- return ( await api.get.taxClasses() ).filter(
- ( { slug } ) =>
- ! [ 'standard', 'reduced-rate', 'zero-rate' ].includes( slug )
- );
- };
-
- while (
- ( taxClasses = await getExistingNonDefaultTaxClasses() ).length > 0
- ) {
- const slugs = taxClasses.map( ( { slug } ) => slug );
- for ( const slug of slugs ) {
- await api.deletePost.taxClass( slug );
- }
- }
-
- console.log( 'Done.' );
-};
-
-const deleteAllTaxRates = async () => {
- console.log( 'Deleting all tax rates...' );
-
- let taxes,
- page = 1;
-
- while (
- ( taxes = await api.get.taxRates( { per_page: 100, page: page++ } ) )
- .length > 0
- ) {
- const ids = taxes.map( ( { id } ) => id );
- await api.deletePost.taxRates( ids );
- }
-
- console.log( 'Done.' );
-};
-
-/**
- * Reset the test site. Useful when running E2E tests on a hosted test site to reset it to a somewhat pristine state prior to running tests.
- *
- * @param {string} cKey Consumer key
- * @param {string} cSecret Consumer secret
- */
-const reset = async ( cKey, cSecret ) => {
- console.log( '--------------------------' );
- console.log( 'Resetting test site...' );
- console.log( '--------------------------' );
-
- api.constructWith( cKey, cSecret );
-
- await deleteAllCoupons();
- await deleteAllProducts();
- await deleteAllProductAttributes();
- await deleteAllProductCategories();
- await deleteAllProductTags();
- await deleteAllOrders();
- await deleteAllShippingClasses();
- await deleteAllShippingZones();
- await deleteAllShippingMethodsInDefaultShippingZone();
- await deleteAllTaxClasses();
- await deleteAllTaxRates();
-};
-
-/**
- * Convert Cart and Checkout pages to shortcode.
- * @param {import('@playwright/test').FullConfig} config
- */
-const useCartCheckoutShortcodes = async ( config ) => {
- /**
- * A WordPress page.
- * @typedef {Object} WPPage
- * @property {number} id
- * @property {string} slug
- */
-
- const { request: apiRequest } = require( '@playwright/test' );
- const { baseURL, userAgent, extraHTTPHeaders } = config.projects[ 0 ].use;
-
- const options = {
- baseURL,
- userAgent,
- extraHTTPHeaders,
- };
- const request = await apiRequest.newContext( options );
-
- // List all pages
- const response_list = await request.get( '/wp-json/wp/v2/pages', {
- data: {
- _fields: [ 'id', 'slug' ],
- },
- failOnStatusCode: true,
- } );
-
- /**
- * @type {WPPage[]}
- */
- const list = await response_list.json();
-
- // Find the cart and checkout pages
- const cart = list.find( ( page ) => page.slug === 'cart' );
- const checkout = list.find( ( page ) => page.slug === 'checkout' );
-
- // Convert their contents to shortcodes
- await request.put( `/wp-json/wp/v2/pages/${ cart.id }`, {
- data: {
- content: {
- raw: '[woocommerce_cart]',
- },
- },
- failOnStatusCode: true,
- } );
- console.log( 'Cart page converted to shortcode.' );
-
- await request.put( `/wp-json/wp/v2/pages/${ checkout.id }`, {
- data: {
- content: {
- raw: '[woocommerce_checkout]',
- },
- },
- failOnStatusCode: true,
- } );
- console.log( 'Checkout page converted to shortcode.' );
-};
-
-module.exports = {
- reset,
- useCartCheckoutShortcodes,
-};
diff --git a/plugins/woocommerce/tests/e2e-pw/README.md b/plugins/woocommerce/tests/e2e-pw/README.md
index 1845f86e60a..22ce3775b18 100644
--- a/plugins/woocommerce/tests/e2e-pw/README.md
+++ b/plugins/woocommerce/tests/e2e-pw/README.md
@@ -38,7 +38,7 @@ Start in the repository root folder:
- `pnpm --filter='@woocommerce/plugin-woocommerce' build` (builds WooCommerce locally)
- `cd plugins/woocommerce` (changes into the WooCommerce plugin folder)
- `pnpm env:start` (starts the `wp-env` based local environment)
-- `pnpm test:e2e-pw` (runs all the tests in headless mode)
+- `pnpm test:e2e` (runs all the tests in headless mode)
To re-create the environment for a fresh state:
@@ -49,17 +49,17 @@ for managing the `wp-env` environment.
Other ways of running tests (make sure you are in the `plugins/woocommerce` folder):
-- `pnpm test:e2e-pw` (usual, headless run)
-- `pnpm test:e2e-pw --headed` (headed -- displaying browser window and test interactions)
-- `pnpm test:e2e-pw --debug` (runs tests in debug mode)
-- `pnpm test:e2e-pw ./tests/e2e-pw/tests/**/basic.spec.js` (runs a single test file - `basic.spec.js` in this case)
-- `pnpm test:e2e-pw --ui` (open tests in [Playwright UI mode](https://playwright.dev/docs/test-ui-mode)). You may need
+- `pnpm test:e2e` (usual, headless run)
+- `pnpm test:e2e --headed` (headed -- displaying browser window and test interactions)
+- `pnpm test:e2e --debug` (runs tests in debug mode)
+- `pnpm test:e2e ./tests/e2e-pw/tests/**/basic.spec.js` (runs a single test file - `basic.spec.js` in this case)
+- `pnpm test:e2e --ui` (open tests in [Playwright UI mode](https://playwright.dev/docs/test-ui-mode)). You may need
to increase the [test timeout](https://playwright.dev/docs/api/class-testconfig#test-config-timeout) by setting
the `DEFAULT_TIMEOUT_OVERRIDE` environment variable like so:
```bash
# Increase test timeout to 3 minutes
- export DEFAULT_TIMEOUT_OVERRIDE=180000 pnpm test:e2e-pw --ui
+ export DEFAULT_TIMEOUT_OVERRIDE=180000 pnpm test:e2e --ui
```
To see all the Playwright options, make sure you are in the `plugins/woocommerce` folder and
@@ -91,6 +91,8 @@ CUSTOMER_PASSWORD='customer.password'
There are some pre-defined environments set in the `tests/e2e-pw/envs` path.
Each folder represents an environment, and contains a setup script, a `playwright.config.js` file and optionally an
encrypted `.env` file.
+Running the tests with one of these environments will decrypt the `.env.enc` file if it exists, execute the setup
+script and then run the tests using the configuration in the `playwright.config.js` file.
To run the tests using one of these environment, you can use the `test:e2e:with-env` script. Some examples:
@@ -104,14 +106,12 @@ pnpm test:e2e:with-env gutenberg-stable
# The envs/default-pressable/.env.enc file will be decrypted into .env and used to set the required environment variables
pnpm test:e2e:with-env default-pressable
-# Runs all the tests with the default environment, similar to running `pnpm test:e2e-pw`
+# Runs all the tests with the default environment. `pnpm test:e2e` already does that, but only runs e2e, ignoring the API tests.
pnpm test:e2e:with-env default
```
-Some of the environments are using encrypted `.env` files.
-To run command includes a decryption step, which requires the `E2E_ENV_KEY` environment variable to be set.
+To decrypt the .env file, the `E2E_ENV_KEY` environment variable must be set.
If you're an a11n you can find the key in the Secret Store.
-
Run with the `E2E_ENV_KEY` environment variable set:
```bash
diff --git a/plugins/woocommerce/tests/e2e-pw/bin/override-wp-env-plugins.js b/plugins/woocommerce/tests/e2e-pw/bin/override-wp-env-plugins.js
deleted file mode 100644
index 6f4fb9bfcdd..00000000000
--- a/plugins/woocommerce/tests/e2e-pw/bin/override-wp-env-plugins.js
+++ /dev/null
@@ -1,39 +0,0 @@
-const fs = require( 'fs' );
-
-const { RELEASE_TAG, ARTIFACT_NAME } = process.env;
-
-if ( ! RELEASE_TAG ) {
- console.error( 'Please set the RELEASE_TAG environment variable!' );
- process.exit( 1 );
-}
-
-if ( ! ARTIFACT_NAME ) {
- console.error( 'Please set the ARTIFACT_NAME environment variable!' );
- process.exit( 1 );
-}
-
-const artifactUrl = `https://github.com/woocommerce/woocommerce/releases/download/${ RELEASE_TAG }/${ ARTIFACT_NAME }`;
-// https://github.com/woocommerce/woocommerce/releases/download/nightly/woocommerce-trunk-nightly.zip
-// https://github.com/woocommerce/woocommerce/releases/download/9.0.0-beta.2/woocommerce.zip
-
-const testEnvPlugins = {
- env: {
- tests: {
- plugins: [],
- },
- },
-};
-const data = fs.readFileSync( '.wp-env.json', 'utf8' );
-const wpEnvConfig = JSON.parse( data );
-testEnvPlugins.env.tests.plugins = wpEnvConfig.env.tests.plugins;
-
-const currentDirEntry = testEnvPlugins.env.tests.plugins.indexOf( '.' );
-
-if ( currentDirEntry !== -1 ) {
- testEnvPlugins.env.tests.plugins[ currentDirEntry ] = artifactUrl;
-}
-
-fs.writeFileSync(
- '.wp-env.override.json',
- JSON.stringify( testEnvPlugins, null, 2 )
-);
diff --git a/plugins/woocommerce/tests/e2e-pw/bin/test-helper-apis.php b/plugins/woocommerce/tests/e2e-pw/bin/test-helper-apis.php
index edbc0aba17e..ca4dbfa68d2 100644
--- a/plugins/woocommerce/tests/e2e-pw/bin/test-helper-apis.php
+++ b/plugins/woocommerce/tests/e2e-pw/bin/test-helper-apis.php
@@ -34,6 +34,16 @@ function register_helper_api() {
'permission_callback' => 'is_allowed',
)
);
+
+ register_rest_route(
+ 'e2e-environment',
+ '/info',
+ array(
+ 'methods' => 'GET',
+ 'callback' => 'get_environment_info',
+ 'permission_callback' => 'is_allowed',
+ )
+ );
}
add_action( 'rest_api_init', 'register_helper_api' );
@@ -107,3 +117,22 @@ function api_update_option( WP_REST_Request $request ) {
function is_allowed() {
return current_user_can( 'manage_options' );
}
+
+/**
+ * Get environment info
+ * @return WP_REST_Response
+ */
+function get_environment_info() {
+ $data['Core'] = get_bloginfo( 'version' );
+ $data['PHP'] = sprintf( '%s.%s', PHP_MAJOR_VERSION, PHP_MINOR_VERSION );
+
+ $all_plugins = get_plugins();
+
+ foreach ( $all_plugins as $plugin_file => $plugin_data ) {
+ if ( is_plugin_active( $plugin_file ) ) {
+ $data[ $plugin_data['Name'] ] = $plugin_data['Version'];
+ }
+ }
+
+ return new WP_REST_Response( $data, 200 );
+}
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ad.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ad.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ad.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ad.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ae.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ae.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ae.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ae.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/af.json b/plugins/woocommerce/tests/e2e-pw/data/countries/af.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/af.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/af.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ag.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ag.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ag.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ag.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ai.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ai.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ai.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ai.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/al.json b/plugins/woocommerce/tests/e2e-pw/data/countries/al.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/al.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/al.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/am.json b/plugins/woocommerce/tests/e2e-pw/data/countries/am.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/am.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/am.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ao.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ao.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ao.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ao.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/aq.json b/plugins/woocommerce/tests/e2e-pw/data/countries/aq.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/aq.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/aq.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ar.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ar.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ar.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ar.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/as.json b/plugins/woocommerce/tests/e2e-pw/data/countries/as.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/as.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/as.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/at.json b/plugins/woocommerce/tests/e2e-pw/data/countries/at.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/at.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/at.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/au.json b/plugins/woocommerce/tests/e2e-pw/data/countries/au.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/au.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/au.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/aw.json b/plugins/woocommerce/tests/e2e-pw/data/countries/aw.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/aw.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/aw.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ax.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ax.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ax.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ax.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/az.json b/plugins/woocommerce/tests/e2e-pw/data/countries/az.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/az.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/az.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ba.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ba.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ba.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ba.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bb.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bb.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bb.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bb.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bd.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bd.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bd.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bd.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/be.json b/plugins/woocommerce/tests/e2e-pw/data/countries/be.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/be.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/be.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bf.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bf.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bf.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bf.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bg.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bg.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bg.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bg.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bh.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bh.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bh.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bh.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bi.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bi.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bi.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bi.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bj.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bj.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bj.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bj.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bl.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bl.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bl.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bl.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bm.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bm.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bm.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bm.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bn.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bn.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bn.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bn.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bo.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bo.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bo.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bo.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bq.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bq.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bq.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bq.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/br.json b/plugins/woocommerce/tests/e2e-pw/data/countries/br.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/br.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/br.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bs.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bs.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bs.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bs.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bt.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bt.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bt.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bt.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bv.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bv.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bv.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bv.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bw.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bw.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bw.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bw.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/by.json b/plugins/woocommerce/tests/e2e-pw/data/countries/by.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/by.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/by.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/bz.json b/plugins/woocommerce/tests/e2e-pw/data/countries/bz.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/bz.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/bz.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ca.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ca.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ca.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ca.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cc.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cc.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cc.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cc.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cd.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cd.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cd.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cd.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cf.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cf.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cf.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cf.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cg.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cg.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cg.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cg.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ch.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ch.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ch.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ch.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ci.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ci.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ci.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ci.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ck.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ck.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ck.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ck.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cl.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cl.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cl.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cl.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cm.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cm.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cm.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cm.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cn.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cn.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cn.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cn.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/co.json b/plugins/woocommerce/tests/e2e-pw/data/countries/co.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/co.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/co.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cr.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cr.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cr.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cr.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cu.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cu.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cu.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cu.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cv.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cv.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cv.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cv.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cw.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cw.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cw.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cw.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cx.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cx.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cx.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cx.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cy.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cy.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cy.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cy.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/cz.json b/plugins/woocommerce/tests/e2e-pw/data/countries/cz.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/cz.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/cz.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/de.json b/plugins/woocommerce/tests/e2e-pw/data/countries/de.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/de.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/de.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/dj.json b/plugins/woocommerce/tests/e2e-pw/data/countries/dj.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/dj.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/dj.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/dk.json b/plugins/woocommerce/tests/e2e-pw/data/countries/dk.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/dk.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/dk.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/dm.json b/plugins/woocommerce/tests/e2e-pw/data/countries/dm.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/dm.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/dm.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/do.json b/plugins/woocommerce/tests/e2e-pw/data/countries/do.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/do.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/do.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/dz.json b/plugins/woocommerce/tests/e2e-pw/data/countries/dz.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/dz.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/dz.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ec.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ec.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ec.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ec.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ee.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ee.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ee.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ee.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/eg.json b/plugins/woocommerce/tests/e2e-pw/data/countries/eg.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/eg.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/eg.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/eh.json b/plugins/woocommerce/tests/e2e-pw/data/countries/eh.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/eh.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/eh.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/er.json b/plugins/woocommerce/tests/e2e-pw/data/countries/er.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/er.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/er.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/es.json b/plugins/woocommerce/tests/e2e-pw/data/countries/es.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/es.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/es.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/et.json b/plugins/woocommerce/tests/e2e-pw/data/countries/et.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/et.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/et.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/fi.json b/plugins/woocommerce/tests/e2e-pw/data/countries/fi.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/fi.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/fi.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/fj.json b/plugins/woocommerce/tests/e2e-pw/data/countries/fj.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/fj.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/fj.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/fk.json b/plugins/woocommerce/tests/e2e-pw/data/countries/fk.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/fk.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/fk.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/fm.json b/plugins/woocommerce/tests/e2e-pw/data/countries/fm.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/fm.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/fm.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/fo.json b/plugins/woocommerce/tests/e2e-pw/data/countries/fo.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/fo.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/fo.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/fr.json b/plugins/woocommerce/tests/e2e-pw/data/countries/fr.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/fr.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/fr.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ga.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ga.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ga.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ga.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gb.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gb.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gb.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gb.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gd.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gd.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gd.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gd.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ge.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ge.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ge.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ge.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gf.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gf.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gf.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gf.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gg.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gg.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gg.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gg.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gh.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gh.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gh.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gh.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gi.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gi.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gi.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gi.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gl.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gl.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gl.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gl.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gm.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gm.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gm.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gm.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gn.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gn.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gn.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gn.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gp.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gp.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gp.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gp.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gq.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gq.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gq.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gq.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gr.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gr.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gr.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gr.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gs.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gs.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gs.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gs.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gt.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gt.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gt.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gt.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gu.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gu.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gu.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gu.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gw.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gw.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gw.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gw.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/gy.json b/plugins/woocommerce/tests/e2e-pw/data/countries/gy.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/gy.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/gy.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/hk.json b/plugins/woocommerce/tests/e2e-pw/data/countries/hk.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/hk.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/hk.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/hm.json b/plugins/woocommerce/tests/e2e-pw/data/countries/hm.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/hm.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/hm.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/hn.json b/plugins/woocommerce/tests/e2e-pw/data/countries/hn.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/hn.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/hn.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/hr.json b/plugins/woocommerce/tests/e2e-pw/data/countries/hr.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/hr.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/hr.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ht.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ht.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ht.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ht.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/hu.json b/plugins/woocommerce/tests/e2e-pw/data/countries/hu.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/hu.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/hu.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/id.json b/plugins/woocommerce/tests/e2e-pw/data/countries/id.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/id.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/id.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ie.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ie.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ie.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ie.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/il.json b/plugins/woocommerce/tests/e2e-pw/data/countries/il.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/il.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/il.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/im.json b/plugins/woocommerce/tests/e2e-pw/data/countries/im.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/im.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/im.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/in.json b/plugins/woocommerce/tests/e2e-pw/data/countries/in.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/in.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/in.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/io.json b/plugins/woocommerce/tests/e2e-pw/data/countries/io.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/io.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/io.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/iq.json b/plugins/woocommerce/tests/e2e-pw/data/countries/iq.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/iq.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/iq.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ir.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ir.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ir.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ir.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/is.json b/plugins/woocommerce/tests/e2e-pw/data/countries/is.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/is.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/is.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/it.json b/plugins/woocommerce/tests/e2e-pw/data/countries/it.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/it.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/it.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/je.json b/plugins/woocommerce/tests/e2e-pw/data/countries/je.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/je.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/je.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/jm.json b/plugins/woocommerce/tests/e2e-pw/data/countries/jm.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/jm.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/jm.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/jo.json b/plugins/woocommerce/tests/e2e-pw/data/countries/jo.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/jo.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/jo.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/jp.json b/plugins/woocommerce/tests/e2e-pw/data/countries/jp.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/jp.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/jp.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ke.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ke.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ke.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ke.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/kg.json b/plugins/woocommerce/tests/e2e-pw/data/countries/kg.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/kg.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/kg.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/kh.json b/plugins/woocommerce/tests/e2e-pw/data/countries/kh.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/kh.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/kh.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ki.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ki.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ki.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ki.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/km.json b/plugins/woocommerce/tests/e2e-pw/data/countries/km.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/km.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/km.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/kn.json b/plugins/woocommerce/tests/e2e-pw/data/countries/kn.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/kn.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/kn.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/kp.json b/plugins/woocommerce/tests/e2e-pw/data/countries/kp.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/kp.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/kp.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/kr.json b/plugins/woocommerce/tests/e2e-pw/data/countries/kr.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/kr.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/kr.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/kw.json b/plugins/woocommerce/tests/e2e-pw/data/countries/kw.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/kw.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/kw.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ky.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ky.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ky.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ky.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/kz.json b/plugins/woocommerce/tests/e2e-pw/data/countries/kz.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/kz.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/kz.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/la.json b/plugins/woocommerce/tests/e2e-pw/data/countries/la.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/la.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/la.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/lb.json b/plugins/woocommerce/tests/e2e-pw/data/countries/lb.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/lb.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/lb.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/lc.json b/plugins/woocommerce/tests/e2e-pw/data/countries/lc.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/lc.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/lc.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/li.json b/plugins/woocommerce/tests/e2e-pw/data/countries/li.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/li.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/li.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/lk.json b/plugins/woocommerce/tests/e2e-pw/data/countries/lk.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/lk.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/lk.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/lr.json b/plugins/woocommerce/tests/e2e-pw/data/countries/lr.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/lr.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/lr.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ls.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ls.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ls.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ls.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/lt.json b/plugins/woocommerce/tests/e2e-pw/data/countries/lt.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/lt.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/lt.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/lu.json b/plugins/woocommerce/tests/e2e-pw/data/countries/lu.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/lu.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/lu.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/lv.json b/plugins/woocommerce/tests/e2e-pw/data/countries/lv.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/lv.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/lv.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ly.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ly.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ly.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ly.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ma.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ma.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ma.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ma.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mc.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mc.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mc.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mc.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/md.json b/plugins/woocommerce/tests/e2e-pw/data/countries/md.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/md.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/md.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/me.json b/plugins/woocommerce/tests/e2e-pw/data/countries/me.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/me.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/me.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mf.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mf.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mf.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mf.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mg.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mg.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mg.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mg.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mh.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mh.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mh.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mh.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mk.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mk.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mk.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mk.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ml.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ml.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ml.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ml.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mm.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mm.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mm.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mm.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mn.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mn.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mn.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mn.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mo.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mo.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mo.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mo.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mp.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mp.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mp.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mp.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mq.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mq.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mq.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mq.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mr.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mr.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mr.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mr.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ms.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ms.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ms.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ms.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mt.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mt.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mt.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mt.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mu.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mu.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mu.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mu.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mv.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mv.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mv.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mv.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mw.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mw.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mw.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mw.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mx.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mx.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mx.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mx.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/my.json b/plugins/woocommerce/tests/e2e-pw/data/countries/my.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/my.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/my.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/mz.json b/plugins/woocommerce/tests/e2e-pw/data/countries/mz.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/mz.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/mz.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/na.json b/plugins/woocommerce/tests/e2e-pw/data/countries/na.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/na.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/na.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/nc.json b/plugins/woocommerce/tests/e2e-pw/data/countries/nc.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/nc.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/nc.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ne.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ne.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ne.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ne.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/nf.json b/plugins/woocommerce/tests/e2e-pw/data/countries/nf.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/nf.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/nf.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ng.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ng.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ng.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ng.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ni.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ni.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ni.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ni.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/nl.json b/plugins/woocommerce/tests/e2e-pw/data/countries/nl.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/nl.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/nl.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/no.json b/plugins/woocommerce/tests/e2e-pw/data/countries/no.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/no.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/no.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/np.json b/plugins/woocommerce/tests/e2e-pw/data/countries/np.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/np.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/np.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/nr.json b/plugins/woocommerce/tests/e2e-pw/data/countries/nr.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/nr.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/nr.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/nu.json b/plugins/woocommerce/tests/e2e-pw/data/countries/nu.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/nu.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/nu.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/nz.json b/plugins/woocommerce/tests/e2e-pw/data/countries/nz.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/nz.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/nz.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/om.json b/plugins/woocommerce/tests/e2e-pw/data/countries/om.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/om.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/om.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/pa.json b/plugins/woocommerce/tests/e2e-pw/data/countries/pa.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/pa.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/pa.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/pe.json b/plugins/woocommerce/tests/e2e-pw/data/countries/pe.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/pe.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/pe.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/pf.json b/plugins/woocommerce/tests/e2e-pw/data/countries/pf.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/pf.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/pf.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/pg.json b/plugins/woocommerce/tests/e2e-pw/data/countries/pg.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/pg.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/pg.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ph.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ph.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ph.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ph.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/pk.json b/plugins/woocommerce/tests/e2e-pw/data/countries/pk.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/pk.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/pk.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/pl.json b/plugins/woocommerce/tests/e2e-pw/data/countries/pl.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/pl.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/pl.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/pm.json b/plugins/woocommerce/tests/e2e-pw/data/countries/pm.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/pm.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/pm.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/pn.json b/plugins/woocommerce/tests/e2e-pw/data/countries/pn.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/pn.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/pn.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/pr.json b/plugins/woocommerce/tests/e2e-pw/data/countries/pr.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/pr.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/pr.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ps.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ps.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ps.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ps.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/pt.json b/plugins/woocommerce/tests/e2e-pw/data/countries/pt.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/pt.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/pt.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/pw.json b/plugins/woocommerce/tests/e2e-pw/data/countries/pw.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/pw.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/pw.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/py.json b/plugins/woocommerce/tests/e2e-pw/data/countries/py.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/py.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/py.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/qa.json b/plugins/woocommerce/tests/e2e-pw/data/countries/qa.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/qa.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/qa.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/re.json b/plugins/woocommerce/tests/e2e-pw/data/countries/re.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/re.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/re.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ro.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ro.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ro.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ro.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/rs.json b/plugins/woocommerce/tests/e2e-pw/data/countries/rs.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/rs.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/rs.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ru.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ru.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ru.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ru.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/rw.json b/plugins/woocommerce/tests/e2e-pw/data/countries/rw.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/rw.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/rw.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sa.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sa.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sa.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sa.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sb.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sb.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sb.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sb.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sc.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sc.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sc.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sc.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sd.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sd.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sd.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sd.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/se.json b/plugins/woocommerce/tests/e2e-pw/data/countries/se.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/se.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/se.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sg.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sg.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sg.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sg.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sh.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sh.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sh.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sh.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/si.json b/plugins/woocommerce/tests/e2e-pw/data/countries/si.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/si.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/si.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sj.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sj.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sj.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sj.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sk.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sk.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sk.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sk.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sl.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sl.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sl.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sl.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sm.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sm.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sm.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sm.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sn.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sn.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sn.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sn.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/so.json b/plugins/woocommerce/tests/e2e-pw/data/countries/so.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/so.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/so.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sr.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sr.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sr.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sr.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ss.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ss.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ss.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ss.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/st.json b/plugins/woocommerce/tests/e2e-pw/data/countries/st.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/st.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/st.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sv.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sv.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sv.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sv.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sx.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sx.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sx.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sx.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sy.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sy.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sy.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sy.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/sz.json b/plugins/woocommerce/tests/e2e-pw/data/countries/sz.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/sz.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/sz.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/tc.json b/plugins/woocommerce/tests/e2e-pw/data/countries/tc.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/tc.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/tc.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/td.json b/plugins/woocommerce/tests/e2e-pw/data/countries/td.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/td.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/td.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/tf.json b/plugins/woocommerce/tests/e2e-pw/data/countries/tf.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/tf.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/tf.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/tg.json b/plugins/woocommerce/tests/e2e-pw/data/countries/tg.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/tg.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/tg.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/th.json b/plugins/woocommerce/tests/e2e-pw/data/countries/th.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/th.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/th.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/tj.json b/plugins/woocommerce/tests/e2e-pw/data/countries/tj.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/tj.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/tj.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/tk.json b/plugins/woocommerce/tests/e2e-pw/data/countries/tk.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/tk.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/tk.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/tl.json b/plugins/woocommerce/tests/e2e-pw/data/countries/tl.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/tl.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/tl.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/tm.json b/plugins/woocommerce/tests/e2e-pw/data/countries/tm.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/tm.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/tm.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/tn.json b/plugins/woocommerce/tests/e2e-pw/data/countries/tn.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/tn.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/tn.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/to.json b/plugins/woocommerce/tests/e2e-pw/data/countries/to.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/to.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/to.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/tr.json b/plugins/woocommerce/tests/e2e-pw/data/countries/tr.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/tr.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/tr.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/tt.json b/plugins/woocommerce/tests/e2e-pw/data/countries/tt.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/tt.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/tt.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/tv.json b/plugins/woocommerce/tests/e2e-pw/data/countries/tv.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/tv.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/tv.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/tw.json b/plugins/woocommerce/tests/e2e-pw/data/countries/tw.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/tw.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/tw.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/tz.json b/plugins/woocommerce/tests/e2e-pw/data/countries/tz.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/tz.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/tz.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ua.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ua.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ua.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ua.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ug.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ug.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ug.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ug.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/um.json b/plugins/woocommerce/tests/e2e-pw/data/countries/um.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/um.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/um.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/us.json b/plugins/woocommerce/tests/e2e-pw/data/countries/us.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/us.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/us.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/uy.json b/plugins/woocommerce/tests/e2e-pw/data/countries/uy.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/uy.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/uy.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/uz.json b/plugins/woocommerce/tests/e2e-pw/data/countries/uz.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/uz.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/uz.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/va.json b/plugins/woocommerce/tests/e2e-pw/data/countries/va.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/va.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/va.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/vc.json b/plugins/woocommerce/tests/e2e-pw/data/countries/vc.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/vc.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/vc.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ve.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ve.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ve.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ve.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/vg.json b/plugins/woocommerce/tests/e2e-pw/data/countries/vg.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/vg.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/vg.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/vi.json b/plugins/woocommerce/tests/e2e-pw/data/countries/vi.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/vi.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/vi.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/vn.json b/plugins/woocommerce/tests/e2e-pw/data/countries/vn.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/vn.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/vn.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/vu.json b/plugins/woocommerce/tests/e2e-pw/data/countries/vu.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/vu.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/vu.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/wf.json b/plugins/woocommerce/tests/e2e-pw/data/countries/wf.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/wf.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/wf.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ws.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ws.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ws.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ws.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/ye.json b/plugins/woocommerce/tests/e2e-pw/data/countries/ye.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/ye.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/ye.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/yt.json b/plugins/woocommerce/tests/e2e-pw/data/countries/yt.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/yt.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/yt.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/za.json b/plugins/woocommerce/tests/e2e-pw/data/countries/za.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/za.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/za.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/zm.json b/plugins/woocommerce/tests/e2e-pw/data/countries/zm.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/zm.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/zm.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/countries/zw.json b/plugins/woocommerce/tests/e2e-pw/data/countries/zw.json
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/countries/zw.json
rename to plugins/woocommerce/tests/e2e-pw/data/countries/zw.json
diff --git a/plugins/woocommerce/tests/api-core-tests/data/coupon.js b/plugins/woocommerce/tests/e2e-pw/data/coupon.js
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/coupon.js
rename to plugins/woocommerce/tests/e2e-pw/data/coupon.js
diff --git a/plugins/woocommerce/tests/e2e-pw/data/customer.js b/plugins/woocommerce/tests/e2e-pw/data/customer.js
new file mode 100644
index 00000000000..1bdf14570bd
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/data/customer.js
@@ -0,0 +1,46 @@
+/**
+ * This file contains objects that can be used as test data for scenarios around creating, retrieivng, updating, and deleting customers.
+ *
+ * For more details on the Product properties, see:
+ *
+ * https://woocommerce.github.io/woocommerce-rest-api-docs/#customers
+ *
+ */
+
+/**
+ * A customer
+ */
+const customer = {
+ email: 'john.doe@example.com',
+ first_name: 'John',
+ last_name: 'Doe',
+ username: 'john.doe',
+ billing: {
+ first_name: 'John',
+ last_name: 'Doe',
+ company: '',
+ address_1: '969 Market',
+ address_2: '',
+ city: 'San Francisco',
+ state: 'CA',
+ postcode: '94103',
+ country: 'US',
+ email: 'john.doe@example.com',
+ phone: '(555) 555-5555',
+ },
+ shipping: {
+ first_name: 'John',
+ last_name: 'Doe',
+ company: '',
+ address_1: '969 Market',
+ address_2: '',
+ city: 'San Francisco',
+ state: 'CA',
+ postcode: '94103',
+ country: 'US',
+ },
+};
+
+module.exports = {
+ customer,
+};
diff --git a/plugins/woocommerce/tests/api-core-tests/data/index.js b/plugins/woocommerce/tests/e2e-pw/data/index.js
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/index.js
rename to plugins/woocommerce/tests/e2e-pw/data/index.js
diff --git a/plugins/woocommerce/tests/api-core-tests/data/order.js b/plugins/woocommerce/tests/e2e-pw/data/order.js
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/order.js
rename to plugins/woocommerce/tests/e2e-pw/data/order.js
diff --git a/plugins/woocommerce/tests/api-core-tests/data/products-crud.js b/plugins/woocommerce/tests/e2e-pw/data/products-crud.js
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/products-crud.js
rename to plugins/woocommerce/tests/e2e-pw/data/products-crud.js
diff --git a/plugins/woocommerce/tests/api-core-tests/data/refund.js b/plugins/woocommerce/tests/e2e-pw/data/refund.js
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/refund.js
rename to plugins/woocommerce/tests/e2e-pw/data/refund.js
diff --git a/plugins/woocommerce/tests/api-core-tests/data/settings.js b/plugins/woocommerce/tests/e2e-pw/data/settings.js
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/settings.js
rename to plugins/woocommerce/tests/e2e-pw/data/settings.js
diff --git a/plugins/woocommerce/tests/api-core-tests/data/shared/batch-update.js b/plugins/woocommerce/tests/e2e-pw/data/shared/batch-update.js
similarity index 95%
rename from plugins/woocommerce/tests/api-core-tests/data/shared/batch-update.js
rename to plugins/woocommerce/tests/e2e-pw/data/shared/batch-update.js
index 440996ca0ca..700943d0d68 100644
--- a/plugins/woocommerce/tests/api-core-tests/data/shared/batch-update.js
+++ b/plugins/woocommerce/tests/e2e-pw/data/shared/batch-update.js
@@ -6,7 +6,6 @@
* @param {string} action Batch action. Must be one of: create, update, or delete.
* @param {Array} resources A list of resource objects. For the delete action, this will be a list of IDs.
* @param {Object} payload The batch payload object. Defaults to an empty object.
- * @return {Object} The payload to send to the batch endpoint.
*/
const batch = ( action, resources = [], payload = {} ) => {
if ( ! [ 'create', 'update', 'delete' ].includes( action ) ) {
diff --git a/plugins/woocommerce/tests/api-core-tests/data/shared/customer.js b/plugins/woocommerce/tests/e2e-pw/data/shared/customer.js
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/shared/customer.js
rename to plugins/woocommerce/tests/e2e-pw/data/shared/customer.js
diff --git a/plugins/woocommerce/tests/api-core-tests/data/shared/error-response.js b/plugins/woocommerce/tests/e2e-pw/data/shared/error-response.js
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/shared/error-response.js
rename to plugins/woocommerce/tests/e2e-pw/data/shared/error-response.js
diff --git a/plugins/woocommerce/tests/api-core-tests/data/shared/index.js b/plugins/woocommerce/tests/e2e-pw/data/shared/index.js
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/shared/index.js
rename to plugins/woocommerce/tests/e2e-pw/data/shared/index.js
diff --git a/plugins/woocommerce/tests/api-core-tests/data/shipping-method.js b/plugins/woocommerce/tests/e2e-pw/data/shipping-method.js
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/shipping-method.js
rename to plugins/woocommerce/tests/e2e-pw/data/shipping-method.js
diff --git a/plugins/woocommerce/tests/api-core-tests/data/shipping-zone.js b/plugins/woocommerce/tests/e2e-pw/data/shipping-zone.js
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/shipping-zone.js
rename to plugins/woocommerce/tests/e2e-pw/data/shipping-zone.js
diff --git a/plugins/woocommerce/tests/e2e-pw/data/tax-rate.js b/plugins/woocommerce/tests/e2e-pw/data/tax-rate.js
new file mode 100644
index 00000000000..50fa2b7cfd7
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/data/tax-rate.js
@@ -0,0 +1,421 @@
+/**
+ * A standard tax rate.
+ *
+ * For more details on the tax rate properties, see:
+ *
+ * https://woocommerce.github.io/woocommerce-rest-api-docs/#tax-rate-properties
+ *
+ */
+const standardTaxRate = {
+ name: 'Standard Rate',
+ rate: '10.0000',
+ class: 'standard',
+};
+
+const reducedTaxRate = {
+ name: 'Reduced Rate',
+ rate: '1.0000',
+ class: 'reduced-rate',
+};
+
+const zeroTaxRate = {
+ name: 'Zero Rate',
+ rate: '0.0000',
+ class: 'zero-rate',
+};
+
+const getTaxRateExamples = () => {
+ return { standardTaxRate, reducedTaxRate, zeroTaxRate };
+};
+
+const allUSTaxesExample = [
+ {
+ country: 'US',
+ state: 'AL',
+ rate: '4.0000',
+ name: 'State Tax',
+ shipping: false,
+ order: 1,
+ },
+ {
+ country: 'US',
+ state: 'AZ',
+ rate: '5.6000',
+ name: 'State Tax',
+ shipping: false,
+ order: 2,
+ },
+ {
+ country: 'US',
+ state: 'AR',
+ rate: '6.5000',
+ name: 'State Tax',
+ shipping: true,
+ order: 3,
+ },
+ {
+ country: 'US',
+ state: 'CA',
+ rate: '7.5000',
+ name: 'State Tax',
+ shipping: false,
+ order: 4,
+ },
+ {
+ country: 'US',
+ state: 'CO',
+ rate: '2.9000',
+ name: 'State Tax',
+ shipping: false,
+ order: 5,
+ },
+ {
+ country: 'US',
+ state: 'CT',
+ rate: '6.3500',
+ name: 'State Tax',
+ shipping: true,
+ order: 6,
+ },
+ {
+ country: 'US',
+ state: 'DC',
+ rate: '5.7500',
+ name: 'State Tax',
+ shipping: true,
+ order: 7,
+ },
+ {
+ country: 'US',
+ state: 'FL',
+ rate: '6.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 8,
+ },
+ {
+ country: 'US',
+ state: 'GA',
+ rate: '4.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 9,
+ },
+ {
+ country: 'US',
+ state: 'GU',
+ rate: '4.0000',
+ name: 'State Tax',
+ shipping: false,
+ order: 10,
+ },
+ {
+ country: 'US',
+ state: 'HI',
+ rate: '4.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 11,
+ },
+ {
+ country: 'US',
+ state: 'ID',
+ rate: '6.0000',
+ name: 'State Tax',
+ shipping: false,
+ order: 12,
+ },
+ {
+ country: 'US',
+ state: 'IL',
+ rate: '6.2500',
+ name: 'State Tax',
+ shipping: false,
+ order: 13,
+ },
+ {
+ country: 'US',
+ state: 'IN',
+ rate: '7.0000',
+ name: 'State Tax',
+ shipping: false,
+ order: 14,
+ },
+ {
+ country: 'US',
+ state: 'IA',
+ rate: '6.0000',
+ name: 'State Tax',
+ shipping: false,
+ order: 15,
+ },
+ {
+ country: 'US',
+ state: 'KS',
+ rate: '6.1500',
+ name: 'State Tax',
+ shipping: true,
+ order: 16,
+ },
+ {
+ country: 'US',
+ state: 'KY',
+ rate: '6.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 17,
+ },
+ {
+ country: 'US',
+ state: 'LA',
+ rate: '4.0000',
+ name: 'State Tax',
+ shipping: false,
+ order: 18,
+ },
+ {
+ country: 'US',
+ state: 'ME',
+ rate: '5.5000',
+ name: 'State Tax',
+ shipping: false,
+ order: 19,
+ },
+ {
+ country: 'US',
+ state: 'MD',
+ rate: '6.0000',
+ name: 'State Tax',
+ shipping: false,
+ order: 20,
+ },
+ {
+ country: 'US',
+ state: 'MA',
+ rate: '6.2500',
+ name: 'State Tax',
+ shipping: false,
+ order: 21,
+ },
+ {
+ country: 'US',
+ state: 'MI',
+ rate: '6.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 22,
+ },
+ {
+ country: 'US',
+ state: 'MN',
+ rate: '6.8750',
+ name: 'State Tax',
+ shipping: true,
+ order: 23,
+ },
+ {
+ country: 'US',
+ state: 'MS',
+ rate: '7.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 24,
+ },
+ {
+ country: 'US',
+ state: 'MO',
+ rate: '4.2250',
+ name: 'State Tax',
+ shipping: false,
+ order: 25,
+ },
+ {
+ country: 'US',
+ state: 'NE',
+ rate: '5.5000',
+ name: 'State Tax',
+ shipping: true,
+ order: 26,
+ },
+ {
+ country: 'US',
+ state: 'NV',
+ rate: '6.8500',
+ name: 'State Tax',
+ shipping: false,
+ order: 27,
+ },
+ {
+ country: 'US',
+ state: 'NJ',
+ rate: '7.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 28,
+ },
+ {
+ country: 'US',
+ state: 'NM',
+ rate: '5.1250',
+ name: 'State Tax',
+ shipping: true,
+ order: 29,
+ },
+ {
+ country: 'US',
+ state: 'NY',
+ rate: '4.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 30,
+ },
+ {
+ country: 'US',
+ state: 'NC',
+ rate: '4.7500',
+ name: 'State Tax',
+ shipping: true,
+ order: 31,
+ },
+ {
+ country: 'US',
+ state: 'ND',
+ rate: '5.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 32,
+ },
+ {
+ country: 'US',
+ state: 'OH',
+ rate: '5.7500',
+ name: 'State Tax',
+ shipping: true,
+ order: 33,
+ },
+ {
+ country: 'US',
+ state: 'OK',
+ rate: '4.5000',
+ name: 'State Tax',
+ shipping: false,
+ order: 34,
+ },
+ {
+ country: 'US',
+ state: 'PA',
+ rate: '6.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 35,
+ },
+ {
+ country: 'US',
+ state: 'PR',
+ rate: '6.0000',
+ name: 'State Tax',
+ shipping: false,
+ order: 36,
+ },
+ {
+ country: 'US',
+ state: 'RI',
+ rate: '7.0000',
+ name: 'State Tax',
+ shipping: false,
+ order: 37,
+ },
+ {
+ country: 'US',
+ state: 'SC',
+ rate: '6.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 38,
+ },
+ {
+ country: 'US',
+ state: 'SD',
+ rate: '4.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 39,
+ },
+ {
+ country: 'US',
+ state: 'TN',
+ rate: '7.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 40,
+ },
+ {
+ country: 'US',
+ state: 'TX',
+ rate: '6.2500',
+ name: 'State Tax',
+ shipping: true,
+ order: 41,
+ },
+ {
+ country: 'US',
+ state: 'UT',
+ rate: '5.9500',
+ name: 'State Tax',
+ shipping: false,
+ order: 42,
+ },
+ {
+ country: 'US',
+ state: 'VT',
+ rate: '6.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 43,
+ },
+ {
+ country: 'US',
+ state: 'VA',
+ rate: '5.3000',
+ name: 'State Tax',
+ shipping: false,
+ order: 44,
+ },
+ {
+ country: 'US',
+ state: 'WA',
+ rate: '6.5000',
+ name: 'State Tax',
+ shipping: true,
+ order: 45,
+ },
+ {
+ country: 'US',
+ state: 'WV',
+ rate: '6.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 46,
+ },
+ {
+ country: 'US',
+ state: 'WI',
+ rate: '5.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 47,
+ },
+ {
+ country: 'US',
+ state: 'WY',
+ rate: '4.0000',
+ name: 'State Tax',
+ shipping: true,
+ order: 48,
+ },
+];
+
+module.exports = {
+ getTaxRateExamples,
+ allUSTaxesExample,
+};
diff --git a/plugins/woocommerce/tests/api-core-tests/data/variation.js b/plugins/woocommerce/tests/e2e-pw/data/variation.js
similarity index 100%
rename from plugins/woocommerce/tests/api-core-tests/data/variation.js
rename to plugins/woocommerce/tests/e2e-pw/data/variation.js
diff --git a/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/.env.enc b/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/.env.enc
index a153abf721b..097877226d4 100644
Binary files a/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/.env.enc and b/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/.env.enc differ
diff --git a/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/playwright.config.js
index e9593a68c53..29e31e30715 100644
--- a/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/playwright.config.js
+++ b/plugins/woocommerce/tests/e2e-pw/envs/default-pressable/playwright.config.js
@@ -3,10 +3,9 @@ const { devices } = require( '@playwright/test' );
config = {
...config,
- retries: 0,
projects: [
{
- name: 'default',
+ name: 'default pressable',
use: { ...devices[ 'Desktop Chrome' ] },
testMatch: '**basic.spec.js',
},
diff --git a/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/.env.enc b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/.env.enc
new file mode 100644
index 00000000000..489ff98abab
Binary files /dev/null and b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/.env.enc differ
diff --git a/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/env-setup.sh b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/env-setup.sh
new file mode 100755
index 00000000000..0a5c3f39547
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/env-setup.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+set -eo pipefail
+
+echo "Default WPCOM site setup."
diff --git a/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js
new file mode 100644
index 00000000000..179fca7b471
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/envs/default-wpcom/playwright.config.js
@@ -0,0 +1,15 @@
+let config = require( '../../playwright.config.js' );
+const { devices } = require( '@playwright/test' );
+
+config = {
+ ...config,
+ projects: [
+ {
+ name: 'default wpcom',
+ use: { ...devices[ 'Desktop Chrome' ] },
+ testMatch: '**basic.spec.js',
+ },
+ ],
+};
+
+module.exports = config;
diff --git a/plugins/woocommerce/tests/e2e-pw/envs/default/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/envs/default/playwright.config.js
index 0d5c093c943..ab29a29a95d 100644
--- a/plugins/woocommerce/tests/e2e-pw/envs/default/playwright.config.js
+++ b/plugins/woocommerce/tests/e2e-pw/envs/default/playwright.config.js
@@ -1,16 +1,5 @@
-let config = require( '../../playwright.config.js' );
-const { devices } = require( '@playwright/test' );
+const config = require( '../../playwright.config.js' );
process.env.USE_WP_ENV = 'true';
-config = {
- ...config,
- projects: [
- {
- name: 'default',
- use: { ...devices[ 'Desktop Chrome' ] },
- },
- ],
-};
-
module.exports = config;
diff --git a/plugins/woocommerce/tests/e2e-pw/fixtures/api-tests-fixtures.js b/plugins/woocommerce/tests/e2e-pw/fixtures/api-tests-fixtures.js
new file mode 100644
index 00000000000..7ed6af6cbfe
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/fixtures/api-tests-fixtures.js
@@ -0,0 +1,13 @@
+const base = require( '@playwright/test' );
+const { admin } = require( '../test-data/data' );
+
+exports.test = base.test.extend( {
+ extraHTTPHeaders: {
+ // Add authorization token to all requests.
+ Authorization: `Basic ${ btoa(
+ `${ admin.username }:${ admin.password }`
+ ) }`,
+ },
+} );
+
+exports.expect = base.expect;
diff --git a/plugins/woocommerce/tests/e2e-pw/global-setup.js b/plugins/woocommerce/tests/e2e-pw/global-setup.js
index 9701d1ab707..bb9f1e182c2 100644
--- a/plugins/woocommerce/tests/e2e-pw/global-setup.js
+++ b/plugins/woocommerce/tests/e2e-pw/global-setup.js
@@ -95,7 +95,7 @@ module.exports = async ( config ) => {
// While we're here, let's add a consumer token for API access
// This step was failing occasionally, and globalsetup doesn't retry, so make it retry
- const nRetries = 5;
+ const nRetries = 3;
for ( let i = 0; i < nRetries; i++ ) {
try {
console.log( 'Trying to add consumer token...' );
@@ -122,6 +122,10 @@ module.exports = async ( config ) => {
console.log(
`Failed to add consumer token. Retrying... ${ i }/${ nRetries }`
);
+ await adminPage.screenshot( {
+ fullPage: true,
+ path: `tests/e2e-pw/test-results/generate-key-fail-${ i }.png`,
+ } );
console.log( e );
}
}
diff --git a/plugins/woocommerce/tests/e2e-pw/playwright.config.js b/plugins/woocommerce/tests/e2e-pw/playwright.config.js
index da24401e541..da6a1dc266e 100644
--- a/plugins/woocommerce/tests/e2e-pw/playwright.config.js
+++ b/plugins/woocommerce/tests/e2e-pw/playwright.config.js
@@ -1,8 +1,14 @@
+const { devices } = require( '@playwright/test' );
require( 'dotenv' ).config( { path: __dirname + '/.env' } );
const testsRootPath = __dirname;
const testsResultsPath = `${ testsRootPath }/test-results`;
+if ( ! process.env.BASE_URL ) {
+ console.log( 'BASE_URL is not set. Using default.' );
+ process.env.BASE_URL = 'http://localhost:8086';
+}
+
const {
ALLURE_RESULTS_DIR,
BASE_URL,
@@ -23,12 +29,6 @@ const reporter = [
`${ testsRootPath }/test-results/allure-results`,
detail: true,
suiteTitle: true,
- environmentInfo: {
- Node: process.version,
- OS: process.platform,
- WP: process.env.WP_VERSION,
- CI: process.env.CI,
- },
},
],
[
@@ -37,6 +37,10 @@ const reporter = [
outputFile: `${ testsRootPath }/test-results/test-results-${ Date.now() }.json`,
},
],
+ [
+ `${ testsRootPath }/reporters/environment-reporter.js`,
+ { outputFolder: `${ testsRootPath }/test-results/allure-results` },
+ ],
];
if ( process.env.CI ) {
@@ -71,7 +75,7 @@ const config = {
reporter,
maxFailures: E2E_MAX_FAILURES ? Number( E2E_MAX_FAILURES ) : 0,
use: {
- baseURL: BASE_URL ?? 'http://localhost:8086',
+ baseURL: BASE_URL,
screenshot: { mode: 'only-on-failure', fullPage: true },
stateDir: `${ testsRootPath }/.state/`,
trace:
@@ -84,7 +88,18 @@ const config = {
navigationTimeout: 20 * 1000,
},
snapshotPathTemplate: '{testDir}/{testFilePath}-snapshots/{arg}',
- projects: [],
+ projects: [
+ {
+ name: 'ui',
+ use: { ...devices[ 'Desktop Chrome' ] },
+ testIgnore: '**/api-tests/**',
+ },
+ {
+ name: 'api',
+ use: { ...devices[ 'Desktop Chrome' ] },
+ testMatch: '**/api-tests/**',
+ },
+ ],
};
module.exports = config;
diff --git a/plugins/woocommerce/tests/e2e-pw/reporters/environment-reporter.js b/plugins/woocommerce/tests/e2e-pw/reporters/environment-reporter.js
new file mode 100644
index 00000000000..888f087c582
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/reporters/environment-reporter.js
@@ -0,0 +1,63 @@
+require( '@playwright/test/reporter' );
+const { request } = require( '@playwright/test' );
+const fs = require( 'fs' );
+const path = require( 'path' );
+const { admin } = require( '../test-data/data' );
+
+class EnvironmentReporter {
+ constructor( options ) {
+ this.reportOptions = options;
+ }
+
+ async onEnd() {
+ console.log( 'Getting environment details' );
+ const { outputFolder } = this.reportOptions;
+
+ if ( ! outputFolder ) {
+ console.log( 'No output folder specified!' );
+ return;
+ }
+
+ const { BASE_URL, CI } = process.env;
+ let environmentData = '';
+
+ if ( CI ) {
+ environmentData += `CI=${ CI }`;
+ }
+
+ try {
+ const wpApi = await request.newContext( {
+ baseURL: BASE_URL,
+ extraHTTPHeaders: {
+ Authorization: `Basic ${ Buffer.from(
+ `${ admin.username }:${ admin.password }`
+ ).toString( 'base64' ) }`,
+ },
+ } );
+
+ const info = await wpApi.get( `/wp-json/e2e-environment/info` );
+
+ if ( info.ok() ) {
+ const data = await info.json();
+ for ( const [ key, value ] of Object.entries( data ) ) {
+ // We need to format the values to be compatible with the Java properties file format
+ environmentData += `\n${ key
+ .replace( / /g, '\\u0020' )
+ .replace( /:/g, '-' ) }=${ value }`;
+ }
+ }
+ } catch ( err ) {
+ console.error( `Error getting environment info: ${ err }` );
+ }
+
+ const filePath = path.resolve( outputFolder, 'environment.properties' );
+
+ try {
+ fs.writeFileSync( filePath, environmentData );
+ } catch ( err ) {
+ console.error( `Error writing environment.properties: ${ err }` );
+ }
+ }
+}
+
+module.exports = EnvironmentReporter;
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/activate-and-setup/core-profiler.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/activate-and-setup/core-profiler.spec.js
index cc595bd2c39..bd90679c90b 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/activate-and-setup/core-profiler.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/activate-and-setup/core-profiler.spec.js
@@ -113,10 +113,14 @@ test.describe( 'Store owner can complete the core profiler', () => {
page.getByRole( 'heading', { name: 'Plugins', exact: true } )
).toBeVisible();
// confirm that some of the optional extensions aren't present
- await expect( page.getByText( 'MailPoet' ) ).toBeHidden();
- await expect( page.getByText( 'Pinterest' ) ).toBeHidden();
await expect(
- page.getByText( 'Google Listings & Ads' )
+ page.getByText( 'MailPoet for WooCommerce', { exact: true } )
+ ).toBeHidden();
+ await expect(
+ page.getByText( 'Pinterest for WooCommerce', { exact: true } )
+ ).toBeHidden();
+ await expect(
+ page.getByText( 'Google for WooCommerce', { exact: true } )
).toBeHidden();
} );
@@ -263,12 +267,18 @@ test.describe( 'Store owner can complete the core profiler', () => {
try {
await page
.getByText(
- 'Drive sales with Google Listings & AdsReach millions of active shoppers across'
+ 'Drive sales with Google for WooCommerceReach millions of active shoppers across'
)
.getByRole( 'checkbox' )
.check( { timeout: 2000 } );
} catch ( e ) {
- console.log( 'Checkbox not present for Google Listings & Ads' );
+ // Temporary fix until rebranding is done.
+ await page
+ .getByText(
+ 'Drive sales with Google Listings & AdsReach millions of active shoppers across'
+ )
+ .getByRole( 'checkbox' )
+ .check( { timeout: 2000 } );
}
await page.getByRole( 'button', { name: 'Continue' } ).click();
} );
@@ -307,8 +317,12 @@ test.describe( 'Store owner can complete the core profiler', () => {
await expect(
page.getByText( 'Pinterest for WooCommerce', { exact: true } )
).toBeVisible();
+
await expect(
- page.getByText( 'Google Listings and Ads', { exact: true } )
+ page.getByText(
+ /(Google for WooCommerce|Google Listings & Ads)/,
+ { exact: true }
+ )
).toBeVisible();
await expect( page.getByText( 'MailPoet' ) ).toBeHidden();
await expect( page.getByText( 'Jetpack' ) ).toBeHidden();
@@ -338,17 +352,15 @@ test.describe( 'Store owner can complete the core profiler', () => {
await test.step( 'Clean up installed extensions', async () => {
await page.goto( 'wp-admin/plugins.php' );
- await page.getByLabel( 'Deactivate Google Listings' ).click();
+ await page.getByLabel( 'Deactivate Google' ).click();
await expect(
page.getByText( 'Plugin deactivated.' )
).toBeVisible();
// delete plugin regularly or, if attempted, accept deleting data as well
try {
- await page.getByLabel( 'Delete Google Listings' ).click();
+ await page.getByLabel( 'Delete Google' ).click();
await expect(
- page.getByText(
- 'Google Listings and Ads was successfully deleted.'
- )
+ page.getByText( 'was successfully deleted.' )
).toBeVisible( { timeout: 5000 } );
} catch ( e ) {
await page
@@ -358,9 +370,7 @@ test.describe( 'Store owner can complete the core profiler', () => {
.getByText( 'The selected plugin has been deleted.' )
.waitFor();
}
- await expect(
- page.getByLabel( 'Delete Google Listings' )
- ).toBeHidden();
+ await expect( page.getByLabel( 'Delete Google' ) ).toBeHidden();
await page.getByLabel( 'Deactivate Pinterest for' ).click();
await expect(
page.getByText( 'Plugin deactivated.' )
@@ -369,9 +379,7 @@ test.describe( 'Store owner can complete the core profiler', () => {
try {
await page.getByLabel( 'Delete Pinterest for' ).click();
await expect(
- page.getByText(
- 'Pinterest for WooCommerce was successfully deleted.'
- )
+ page.getByText( 'was successfully deleted.' )
).toBeVisible( { timeout: 5000 } );
} catch ( e ) {
await page
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/coupons/coupons.test.js
similarity index 98%
rename from plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/coupons/coupons.test.js
index 524fbd9670b..7c9a8a06565 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/coupons/coupons.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/coupons/coupons.test.js
@@ -1,12 +1,6 @@
-const { test, expect } = require( '@playwright/test' );
-const { coupon, order } = require( '../../data' );
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
+const { coupon, order } = require( '../../../data' );
-/**
- * Tests for the WooCommerce Coupons API.
- *
- * @group api
- * @group coupons
- */
test.describe( 'Coupons API tests', () => {
//create variable to store the coupon id we will be using
let couponId;
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/customers/customers-crud.test.js
similarity index 96%
rename from plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/customers/customers-crud.test.js
index b83a0c65466..17be7e0a3bc 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/customers/customers-crud.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/customers/customers-crud.test.js
@@ -1,15 +1,7 @@
-const { test, expect } = require( '@playwright/test' );
-const { customer } = require( '../../data' );
-const { USER_KEY } = process.env;
-const userKey = USER_KEY ?? 'admin';
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
+const { admin } = require( '../../../test-data/data' );
+const { customer } = require( '../../../data' );
-/**
- * Tests for the WooCommerce Customers API.
- *
- * @group api
- * @group customers
- *
- */
test.describe( 'Customers API tests: CRUD', () => {
let customerId;
let subscriberUserId;
@@ -60,7 +52,9 @@ test.describe( 'Customers API tests: CRUD', () => {
`/wp-json/wc/v3/customers/${ subscriberUserId }`
);
const responseJSON = await response.json();
+ // eslint-disable-next-line jest/no-standalone-expect
expect( response.status() ).toEqual( 200 );
+ // eslint-disable-next-line jest/no-standalone-expect
expect( responseJSON.role ).toEqual( 'subscriber' );
} );
@@ -97,7 +91,7 @@ test.describe( 'Customers API tests: CRUD', () => {
expect( responseJSON.is_paying_customer ).toEqual( false );
expect( responseJSON.role ).toEqual( 'administrator' );
// this test was updated to allow for local test setup and other test sites.
- expect( responseJSON.username ).toEqual( userKey );
+ expect( responseJSON.username ).toEqual( admin.username );
} );
test( 'can retrieve subscriber user', async ( { request } ) => {
@@ -180,7 +174,9 @@ test.describe( 'Customers API tests: CRUD', () => {
expect( responseJSON.role ).toEqual( 'customer' );
} );
- test( 'can retrieve all customers', async ( { request } ) => {
+ test( 'can retrieve all customers after create', async ( {
+ request,
+ } ) => {
// call API to retrieve all customers
const response = await request.get( '/wp-json/wc/v3/customers' );
const responseJSON = await response.json();
@@ -493,12 +489,12 @@ test.describe( 'Customers API tests: CRUD', () => {
expect( deletedCustomerIds ).toEqual( customerIdsToDelete );
// Verify that the 2 deleted customers cannot be retrieved.
- for ( const customerId of customerIdsToDelete ) {
+ for ( const id of customerIdsToDelete ) {
//Call the API to attempte to retrieve the customers
- const response = await request.get(
- `wp-json/wc/v3/customers/${ customerId }`
+ const r = await request.get(
+ `wp-json/wc/v3/customers/${ id }`
);
- expect( response.status() ).toEqual( 404 );
+ expect( r.status() ).toEqual( 404 );
}
} );
} );
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/data/data-crud.test.js
similarity index 99%
rename from plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/data/data-crud.test.js
index 7ff5a9ad8cd..174cd80b1c7 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/data/data-crud.test.js
@@ -1,4 +1,4 @@
-const { test, expect } = require( '@playwright/test' );
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
// 259 countries total
const countryCodes = [
@@ -253,13 +253,6 @@ const countryCodes = [
'zw',
];
-/**
- * Tests for the WooCommerce Refunds API.
- *
- * @group api
- * @group data
- *
- */
test.describe( 'Data API tests', () => {
test( 'can list all data', async ( { request } ) => {
// call API to retrieve data values
@@ -3973,7 +3966,7 @@ test.describe( 'Data API tests', () => {
// loop through all the countries and validate against the expected data
for ( const country of countryCodes ) {
- const countryData = require( `../../data/countries/${ country }.json` );
+ const countryData = require( `../../../data/countries/${ country }.json` );
expect( responseJSON ).toEqual(
expect.arrayContaining( [
expect.objectContaining( {
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/hello/hello.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/hello/hello.test.js
similarity index 75%
rename from plugins/woocommerce/tests/api-core-tests/tests/hello/hello.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/hello/hello.test.js
index e3ed698019d..0e74a103cc8 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/hello/hello.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/hello/hello.test.js
@@ -1,11 +1,5 @@
-const { test, expect } = require( '@playwright/test' );
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
-/**
- * Tests to verify connection to the API.
- *
- * @group api
- * @group hello
- */
test.describe( 'Test API connectivity', () => {
test( 'can access a non-authenticated endpoint', async ( { request } ) => {
const result = await request.get( '/wp-json/wc/v3/' );
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/orders/order-complex.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-complex.test.js
similarity index 96%
rename from plugins/woocommerce/tests/api-core-tests/tests/orders/order-complex.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-complex.test.js
index 392c89d340a..5308ecdcfa1 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/orders/order-complex.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-complex.test.js
@@ -1,4 +1,4 @@
-const { test, expect } = require( '@playwright/test' );
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
const {
getOrderExample,
@@ -8,7 +8,7 @@ const {
variableProduct: defaultVariableProduct,
groupedProduct: defaultGroupedProduct,
externalProduct: defaultExternalProduct,
-} = require( '../../data' );
+} = require( '../../../data' );
/**
* Simple product with Standard tax rate
@@ -73,14 +73,6 @@ const expectedSimpleProductTaxTotal = '1.00';
const expectedVariableProductTaxTotal = '0.20';
const expectedExternalProductTaxTotal = '0.00';
-/**
- *
- * Test for adding a complex order with different product types and tax classes.
- *
- * @group api
- * @group orders
- *
- */
test.describe( 'Orders API test', () => {
test.beforeAll( async ( { request } ) => {
/**
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/orders/order-search.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-search.test.js
similarity index 82%
rename from plugins/woocommerce/tests/api-core-tests/tests/orders/order-search.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-search.test.js
index e7d1f400a58..ab57435a3b7 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/orders/order-search.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/order-search.test.js
@@ -1,10 +1,10 @@
-const { test, expect } = require( '@playwright/test' );
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
-const { getOrderExampleSearchTest } = require( '../../data/order' );
-const { customerShippingSearchTest } = require( '../../data/shared/customer' );
+const { getOrderExampleSearchTest } = require( '../../../data/order' );
const {
- simpleProduct,
-} = require('../../data/products-crud');
+ customerShippingSearchTest,
+} = require( '../../../data/shared/customer' );
+const { simpleProduct } = require( '../../../data/products-crud' );
/**
* Order to be searched
@@ -49,23 +49,16 @@ const searchParams = [
[ 'shipping state', order.shipping.state ],
];
-/**
- * Tests for the WooCommerce Order Search API.
- *
- * @group api
- * @group orders
- *
- */
test.describe( 'Order Search API tests', () => {
test.beforeAll( async ( { request } ) => {
// Create a product to be associated with the order
- const productResponse = await request.post('wp-json/wc/v3/products', {
+ const productResponse = await request.post( 'wp-json/wc/v3/products', {
data: simpleProduct,
- });
+ } );
const productResponseJSON = await productResponse.json();
// Save the created product id against the order line_items
- order.line_items[0].product_id = productResponseJSON.id;
+ order.line_items[ 0 ].product_id = productResponseJSON.id;
// Create an order and save its ID
const response = await request.post( '/wp-json/wc/v3/orders', {
@@ -77,9 +70,12 @@ test.describe( 'Order Search API tests', () => {
test.afterAll( async ( { request } ) => {
// Cleanup: Delete the product
- await request.delete( `/wp-json/wc/v3/products/${ order.line_items[0].product_id }`, {
- data: { force: true },
- } );
+ await request.delete(
+ `/wp-json/wc/v3/products/${ order.line_items[ 0 ].product_id }`,
+ {
+ data: { force: true },
+ }
+ );
// Cleanup: Delete the order
await request.delete( `/wp-json/wc/v3/orders/${ order.id }`, {
data: { force: true },
@@ -94,6 +90,7 @@ test.describe( 'Order Search API tests', () => {
request,
} ) => {
const searchValue =
+ // eslint-disable-next-line playwright/no-conditional-in-test
searchParamRow[ paramIndex ] === 'orderId'
? order.id
: searchParamRow[ paramIndex ];
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/orders/orders-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders-crud.test.js
similarity index 97%
rename from plugins/woocommerce/tests/api-core-tests/tests/orders/orders-crud.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders-crud.test.js
index 005e3b33d15..7bca8c2836d 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/orders/orders-crud.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders-crud.test.js
@@ -1,7 +1,7 @@
-const { test, expect } = require( '@playwright/test' );
-const { order } = require( '../../data' );
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
+const { order } = require( '../../../data' );
const { API_BASE_URL } = process.env;
-const shouldSkip = API_BASE_URL != undefined;
+const shouldSkip = API_BASE_URL !== undefined;
/**
* Billing properties to update.
@@ -57,13 +57,6 @@ const simpleProduct = {
regular_price: '48',
};
-/**
- * Tests for the WooCommerce Orders API.
- *
- * @group api
- * @group orders
- *
- */
test.describe.serial( 'Orders API tests: CRUD', () => {
let orderId;
@@ -264,6 +257,7 @@ test.describe.serial( 'Orders API tests: CRUD', () => {
test( `can update status of an order to ${ expectedOrderStatus }`, async ( {
request,
} ) => {
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( shouldSkip ) {
await delay( 1000 ); // if this runs too fast on an external host, it fails
}
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/orders/orders.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders.test.js
similarity index 99%
rename from plugins/woocommerce/tests/api-core-tests/tests/orders/orders.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders.test.js
index 0dacf9751d9..f0164b40982 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/orders/orders.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/orders/orders.test.js
@@ -1,5 +1,5 @@
-const { test, expect } = require( '@playwright/test' );
-const { order } = require( '../../data' );
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
+const { order } = require( '../../../data' );
/**
* Billing properties to update.
@@ -34,14 +34,6 @@ const updatedCustomerShipping = {
phone: '123456789',
};
-/**
- * Tests for the WooCommerce Orders API.
- *
- * @group api
- * @group orders
- *
- */
-
test.describe.serial( 'Orders API tests', () => {
let orderId, sampleData;
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/payment-gateways/payment-gateways-crud.test.js
similarity index 96%
rename from plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/payment-gateways/payment-gateways-crud.test.js
index 70ef9183390..c4d021c65f5 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/payment-gateways/payment-gateways-crud.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/payment-gateways/payment-gateways-crud.test.js
@@ -1,12 +1,5 @@
-const { test, expect } = require( '@playwright/test' );
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
-/**
- * Tests for the WooCommerce Refunds API.
- *
- * @group api
- * @group payment gateways
- *
- */
test.describe( 'Payment Gateways API tests', () => {
test( 'can view all payment gateways', async ( { request } ) => {
// call API to retrieve the payment gateways
@@ -16,8 +9,9 @@ test.describe( 'Payment Gateways API tests', () => {
expect( Array.isArray( responseJSON ) ).toBe( true );
const localPickupKey =
- process.env.API_BASE_URL &&
- ! process.env.API_BASE_URL.includes( 'localhost' )
+ // eslint-disable-next-line playwright/no-conditional-in-test
+ process.env.BASE_URL &&
+ ! process.env.BASE_URL.includes( 'localhost' )
? 'pickup_location'
: 'local_pickup';
console.log( 'localPickupKey=', localPickupKey );
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/products/product-list.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/product-list.test.js
similarity index 99%
rename from plugins/woocommerce/tests/api-core-tests/tests/products/product-list.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/product-list.test.js
index b9114779f1a..03f51112b37 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/products/product-list.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/product-list.test.js
@@ -1,15 +1,5 @@
-const { test, expect } = require( '@playwright/test' );
-/**
- * Internal dependencies
- */
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
-/**
- * Tests for the WooCommerce "List all products" API.
- *
- * @group api
- * @group products
- *
- */
test.describe( 'Products API tests: List All Products', () => {
const PRODUCTS_COUNT = 20;
let sampleData;
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/products-crud.test.js
similarity index 97%
rename from plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/products-crud.test.js
index 64d718213f4..4304dcdedfc 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/products/products-crud.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/products/products-crud.test.js
@@ -1,6 +1,6 @@
-const { test, expect } = require( '@playwright/test' );
-const { API_BASE_URL } = process.env;
-const shouldSkip = API_BASE_URL != undefined;
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
+const { BASE_URL } = process.env;
+const shouldSkip = BASE_URL !== undefined;
/**
* Internal dependencies
@@ -9,17 +9,9 @@ const {
simpleProduct,
virtualProduct,
variableProduct,
-} = require( '../../data/products-crud' );
-const { batch } = require( '../../data/shared/batch-update' );
+} = require( '../../../data/products-crud' );
+const { batch } = require( '../../../data/shared/batch-update' );
-/**
- * Tests for the WooCommerce Products API.
- * These tests cover API endpoints for creating, retrieving, updating, and deleting a single product.
- *
- * @group api
- * @group products
- *
- */
test.describe( 'Products API tests: CRUD', () => {
let productId;
@@ -468,7 +460,9 @@ test.describe( 'Products API tests: CRUD', () => {
expect( responseJSON.count ).toEqual( 0 );
} );
- test( 'can permanently delete a product tag', async ( { request } ) => {
+ test( 'can permanently delete a product category', async ( {
+ request,
+ } ) => {
// Delete the product category.
const response = await request.delete(
`wp-json/wc/v3/products/categories/${ productCategoryId }`,
@@ -717,12 +711,7 @@ test.describe( 'Products API tests: CRUD', () => {
const getDeletedProductReviewResponse = await request.get(
`wp-json/wc/v3/products/reviews/${ productReviewId }`
);
- /**
- * currently returns a 403 (forbidden) rather than a 404 (not found)
- * an issue has been raised to track this
- * See: https://github.com/woocommerce/woocommerce/issues/35162
- */
- expect( getDeletedProductReviewResponse.status() ).toEqual( 403 );
+ expect( getDeletedProductReviewResponse.status() ).toEqual( 404 );
} );
test( 'can batch update product reviews', async ( { request } ) => {
@@ -817,12 +806,7 @@ test.describe( 'Products API tests: CRUD', () => {
const getDeletedProductReviewResponse = await request.get(
`wp-json/wc/v3/products/reviews/${ review2Id }`
);
- /**
- * currently returns a 403 (forbidden) rather than a 404 (not found)
- * an issue has been raised to track this
- * See: https://github.com/woocommerce/woocommerce/issues/35162
- */
- expect( getDeletedProductReviewResponse.status() ).toEqual( 403 );
+ expect( getDeletedProductReviewResponse.status() ).toEqual( 404 );
// Batch delete the created tags
await request.post( `wp-json/wc/v3/products/reviews/batch`, {
@@ -1289,6 +1273,7 @@ test.describe( 'Products API tests: CRUD', () => {
expect( response.status() ).toEqual( 200 );
// if we're running on CI, then skip -- because objects are cached and they don't disappear instantly.
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( ! shouldSkip ) {
// Verify that the product variation can no longer be retrieved.
const getDeletedProductVariationResponse = await request.get(
@@ -1378,6 +1363,7 @@ test.describe( 'Products API tests: CRUD', () => {
);
// if we're running on CI, then skip -- because objects are cached and they don't disappear instantly.
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( ! shouldSkip ) {
// Verify that the deleted product variation can no longer be retrieved.
const getDeletedProductVariationResponse = await request.get(
@@ -1534,12 +1520,9 @@ test.describe( 'Products API tests: CRUD', () => {
// Send request to batch delete the products created earlier
const idsToDelete = expectedProducts.map( ( { id } ) => id );
const batchDeletePayload = batch( 'delete', idsToDelete );
- const response = await request.post(
- 'wp-json/wc/v3/products/batch',
- {
- data: batchDeletePayload,
- }
- );
+ let response = await request.post( 'wp-json/wc/v3/products/batch', {
+ data: batchDeletePayload,
+ } );
const responseJSON = await response.json();
const actualBatchDeletedProducts = responseJSON.delete;
@@ -1556,7 +1539,7 @@ test.describe( 'Products API tests: CRUD', () => {
// Verify that the deleted product ID's can no longer be retrieved
for ( const id of idsToDelete ) {
- const response = await request.get(
+ response = await request.get(
`wp-json/wc/v3/products/${ id }`
);
expect( response.status() ).toEqual( 404 );
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/refunds/refunds.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/refunds/refunds.test.js
similarity index 96%
rename from plugins/woocommerce/tests/api-core-tests/tests/refunds/refunds.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/refunds/refunds.test.js
index bf325996f8f..c7af313f3be 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/refunds/refunds.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/refunds/refunds.test.js
@@ -1,13 +1,6 @@
-const { test, expect } = require( '@playwright/test' );
-const { refund } = require( '../../data' );
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
+const { refund } = require( '../../../data' );
-/**
- * Tests for the WooCommerce Refunds API.
- *
- * @group api
- * @group refunds
- *
- */
test.describe( 'Refunds API tests', () => {
let expectedRefund;
let orderId;
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/reports/reports-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/reports/reports-crud.test.js
new file mode 100644
index 00000000000..afa29442805
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/reports/reports-crud.test.js
@@ -0,0 +1,425 @@
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
+
+test.describe( 'Reports API tests', () => {
+ test( 'can view all reports', async ( { request } ) => {
+ // call API to retrieve the reports
+ const response = await request.get( '/wp-json/wc/v3/reports' );
+ const responseJSON = await response.json();
+ expect( response.status() ).toEqual( 200 );
+ expect( Array.isArray( responseJSON ) ).toBe( true );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'sales',
+ description: 'List of sales reports.',
+ } ),
+ ] )
+ );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'sales',
+ description: 'List of sales reports.',
+ } ),
+ ] )
+ );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'sales',
+ description: 'List of sales reports.',
+ } ),
+ ] )
+ );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'sales',
+ description: 'List of sales reports.',
+ } ),
+ ] )
+ );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'sales',
+ description: 'List of sales reports.',
+ } ),
+ ] )
+ );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'sales',
+ description: 'List of sales reports.',
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'sales',
+ description: 'List of sales reports.',
+ } ),
+ ] )
+ );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'sales',
+ description: 'List of sales reports.',
+ } ),
+ ] )
+ );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'sales',
+ description: 'List of sales reports.',
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'sales',
+ description: 'List of sales reports.',
+ } ),
+ ] )
+ );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'sales',
+ description: 'List of sales reports.',
+ } ),
+ ] )
+ );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'sales',
+ description: 'List of sales reports.',
+ } ),
+ ] )
+ );
+ } );
+
+ test( 'can view sales reports', async ( { request } ) => {
+ // call API to retrieve the sales reports
+ const response = await request.get( '/wp-json/wc/v3/reports/sales' );
+ const responseJSON = await response.json();
+ expect( response.status() ).toEqual( 200 );
+ expect( Array.isArray( responseJSON ) ).toBe( true );
+
+ const today = new Date();
+ const dd = String( today.getDate() ).padStart( 2, '0' );
+ const mm = String( today.getMonth() + 1 ).padStart( 2, '0' ); //January is 0!
+ const yyyy = today.getFullYear();
+ const dateString = yyyy + '-' + mm + '-' + dd;
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ total_sales: expect.any( String ),
+ net_sales: expect.any( String ),
+ average_sales: expect.any( String ),
+ total_orders: expect.any( Number ),
+ total_items: expect.any( Number ),
+ total_tax: expect.any( String ),
+ total_shipping: expect.any( String ),
+ total_refunds: expect.any( Number ),
+ total_discount: expect.any( String ),
+ totals_grouped_by: 'day',
+ totals: expect.objectContaining( {
+ [ dateString ]: {
+ sales: expect.any( String ),
+ orders: expect.any( Number ),
+ items: expect.any( Number ),
+ tax: expect.any( String ),
+ shipping: expect.any( String ),
+ discount: expect.any( String ),
+ customers: expect.any( Number ),
+ },
+ } ),
+ total_customers: expect.any( Number ),
+ } ),
+ ] )
+ );
+ } );
+
+ test( 'can view top sellers reports', async ( { request } ) => {
+ // call API to retrieve the top sellers
+ const response = await request.get(
+ '/wp-json/wc/v3/reports/top_sellers'
+ );
+ const responseJSON = await response.json();
+ expect( response.status() ).toEqual( 200 );
+ expect( Array.isArray( responseJSON ) ).toBe( true );
+
+ expect( responseJSON ).toEqual( expect.arrayContaining( [] ) );
+ } );
+
+ test( 'can view coupons totals', async ( { request } ) => {
+ // call API to retrieve the coupons totals
+ const response = await request.get(
+ '/wp-json/wc/v3/reports/coupons/totals'
+ );
+ const responseJSON = await response.json();
+ expect( response.status() ).toEqual( 200 );
+ expect( Array.isArray( responseJSON ) ).toBe( true );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'percent',
+ name: 'Percentage discount',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'fixed_cart',
+ name: 'Fixed cart discount',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'fixed_product',
+ name: 'Fixed product discount',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ } );
+
+ test( 'can view customers totals', async ( { request } ) => {
+ // call API to retrieve the customers totals
+ const response = await request.get(
+ '/wp-json/wc/v3/reports/customers/totals'
+ );
+ const responseJSON = await response.json();
+ expect( response.status() ).toEqual( 200 );
+ expect( Array.isArray( responseJSON ) ).toBe( true );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'paying',
+ name: 'Paying customer',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'non_paying',
+ name: 'Non-paying customer',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ } );
+
+ test( 'can view orders totals', async ( { request } ) => {
+ // call API to retrieve the orders totals
+ const response = await request.get(
+ '/wp-json/wc/v3/reports/orders/totals'
+ );
+ const responseJSON = await response.json();
+ expect( response.status() ).toEqual( 200 );
+ expect( Array.isArray( responseJSON ) ).toBe( true );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'pending',
+ name: 'Pending payment',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'processing',
+ name: 'Processing',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'on-hold',
+ name: 'On hold',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'completed',
+ name: 'Completed',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'cancelled',
+ name: 'Cancelled',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'refunded',
+ name: 'Refunded',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'failed',
+ name: 'Failed',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'checkout-draft',
+ name: 'Draft',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ } );
+
+ test( 'can view products totals', async ( { request } ) => {
+ // call API to retrieve the products totals
+ const response = await request.get(
+ '/wp-json/wc/v3/reports/products/totals'
+ );
+ const responseJSON = await response.json();
+ expect( response.status() ).toEqual( 200 );
+ expect( Array.isArray( responseJSON ) ).toBe( true );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'external',
+ name: 'External/Affiliate product',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'grouped',
+ name: 'Grouped product',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'simple',
+ name: 'Simple product',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'variable',
+ name: 'Variable product',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ } );
+
+ test( 'can view reviews totals', async ( { request } ) => {
+ // call API to retrieve the reviews totals
+ const response = await request.get(
+ '/wp-json/wc/v3/reports/reviews/totals'
+ );
+ const responseJSON = await response.json();
+ expect( response.status() ).toEqual( 200 );
+ expect( Array.isArray( responseJSON ) ).toBe( true );
+
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'rated_1_out_of_5',
+ name: 'Rated 1 out of 5',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'rated_2_out_of_5',
+ name: 'Rated 2 out of 5',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'rated_3_out_of_5',
+ name: 'Rated 3 out of 5',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'rated_4_out_of_5',
+ name: 'Rated 4 out of 5',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ expect( responseJSON ).toEqual(
+ expect.arrayContaining( [
+ expect.objectContaining( {
+ slug: 'rated_5_out_of_5',
+ name: 'Rated 5 out of 5',
+ total: expect.any( Number ),
+ } ),
+ ] )
+ );
+ } );
+} );
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/settings/settings-crud.test.js
similarity index 99%
rename from plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/settings/settings-crud.test.js
index e22be453720..376d606c745 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/settings/settings-crud.test.js
@@ -1,20 +1,12 @@
-const { test, expect } = require( '@playwright/test' );
-const { API_BASE_URL } = process.env;
-const shouldSkip = API_BASE_URL !== undefined;
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
+const { BASE_URL } = process.env;
+const shouldSkip = BASE_URL !== undefined;
const {
countries,
currencies,
stateOptions,
-} = require( '../../data/settings' );
-
-/**
- * Tests for the WooCommerce API.
- *
- * @group api
- * @group settings
- *
- */
+} = require( '../../../data/settings' );
test.describe.serial( 'Settings API tests: CRUD', () => {
test.describe( 'List all settings groups', () => {
@@ -376,6 +368,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => {
);
// different on external host
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( ! shouldSkip ) {
expect( responseJSON ).toEqual(
expect.arrayContaining( [
@@ -395,6 +388,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => {
}
// different on external host
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( ! shouldSkip ) {
expect( responseJSON ).toEqual(
expect.arrayContaining( [
@@ -436,6 +430,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => {
);
// different on external host
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( ! shouldSkip ) {
expect( responseJSON ).toEqual(
expect.arrayContaining( [
@@ -517,6 +512,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => {
] )
);
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( ! shouldSkip ) {
expect( responseJSON ).toEqual(
expect.arrayContaining( [
@@ -1628,6 +1624,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => {
expect( Array.isArray( responseJSON ) ).toBe( true );
// not present in external host
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( ! shouldSkip ) {
expect( responseJSON ).toEqual(
expect.arrayContaining( [
@@ -1647,6 +1644,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => {
}
// not present in external host
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( ! shouldSkip ) {
expect( responseJSON ).toEqual(
expect.arrayContaining( [
@@ -1849,6 +1847,7 @@ test.describe.serial( 'Settings API tests: CRUD', () => {
} ),
] )
);
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( ! shouldSkip ) {
expect( responseJSON ).toEqual(
expect.arrayContaining( [
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/shipping/shipping-method.test.js
similarity index 98%
rename from plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/shipping/shipping-method.test.js
index 72ac2b705f7..45f4d6abb96 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-method.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/shipping/shipping-method.test.js
@@ -1,12 +1,9 @@
/* eslint-disable */
-const {
- test,
- expect
-} = require('@playwright/test');
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
const {
getShippingMethodExample
-} = require('../../data');
+} = require('../../../data');
/**
* Shipping zone id for "Locations not covered by your other zones".
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/shipping/shipping-zones.test.js
similarity index 97%
rename from plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/shipping/shipping-zones.test.js
index 87769591c78..18cfe57515e 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/shipping/shipping-zones.test.js
@@ -1,8 +1,8 @@
/* eslint-disable */
-const { test, expect } = require('@playwright/test');
-const { getShippingZoneExample } = require( '../../data' );
-const { API_BASE_URL } = process.env;
-const shouldSkip = API_BASE_URL != undefined;
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
+const { getShippingZoneExample } = require( '../../../data' );
+const { BASE_URL } = process.env;
+const shouldSkip = BASE_URL !== undefined;
/**
* Tests for the WooCommerce Shipping zones API.
* @group api
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/system-status/system-status-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/system-status/system-status-crud.test.js
similarity index 98%
rename from plugins/woocommerce/tests/api-core-tests/tests/system-status/system-status-crud.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/system-status/system-status-crud.test.js
index c56d661987e..65d56a07196 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/system-status/system-status-crud.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/system-status/system-status-crud.test.js
@@ -1,15 +1,6 @@
-const { test, expect } = require( '@playwright/test' );
-const { API_BASE_URL } = process.env;
-const shouldSkip =
- API_BASE_URL !== undefined && ! API_BASE_URL.includes( 'localhost' );
-
-/**
- * Tests for the WooCommerce API.
- *
- * @group api
- * @group system status
- *
- */
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
+const { BASE_URL } = process.env;
+const shouldSkip = BASE_URL !== undefined && ! BASE_URL.includes( 'localhost' );
test.describe( 'System Status API tests', () => {
test( 'can view all system status items', async ( { request } ) => {
@@ -19,6 +10,7 @@ test.describe( 'System Status API tests', () => {
expect( response.status() ).toEqual( 200 );
// local environment differs from external hosts. Local listed first.
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( ! shouldSkip ) {
expect( responseJSON ).toEqual(
expect.objectContaining( {
@@ -408,6 +400,7 @@ test.describe( 'System Status API tests', () => {
);
// local environment differs from external hosts. Local listed first.
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( ! shouldSkip ) {
expect( responseJSON ).toEqual(
expect.objectContaining( {
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-classes-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/taxes/tax-classes-crud.test.js
similarity index 96%
rename from plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-classes-crud.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/taxes/tax-classes-crud.test.js
index 013d176eb8d..d61932381b4 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-classes-crud.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/taxes/tax-classes-crud.test.js
@@ -1,12 +1,5 @@
-const { test, expect } = require( '@playwright/test' );
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
-/**
- * Tests for the WooCommerce API.
- *
- * @group api
- * @group taxes
- *
- */
test.describe( 'Tax Classes API tests: CRUD', () => {
let taxClassSlug;
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-rates-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/taxes/tax-rates-crud.test.js
similarity index 93%
rename from plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-rates-crud.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/taxes/tax-rates-crud.test.js
index 21ce81cdb92..ae03ca46efc 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/taxes/tax-rates-crud.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/taxes/tax-rates-crud.test.js
@@ -1,15 +1,7 @@
-const { test, expect } = require( '@playwright/test' );
-const { allUSTaxesExample } = require( '../../data' );
-const { API_BASE_URL } = process.env;
-const shouldSkip = API_BASE_URL != undefined;
-
-/**
- * Tests for the WooCommerce API.
- *
- * @group api
- * @group taxes
- *
- */
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
+const { allUSTaxesExample } = require( '../../../data' );
+const { BASE_URL } = process.env;
+const shouldSkip = BASE_URL !== undefined;
test.describe.serial( 'Tax Rates API tests: CRUD', () => {
let taxRateId;
@@ -140,6 +132,7 @@ test.describe.serial( 'Tax Rates API tests: CRUD', () => {
expect( response.status() ).toEqual( 200 );
// only run this test on wp-env -- with external hosting there is caching
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( ! shouldSkip ) {
// Verify that the tax rate can no longer be retrieved.
const getDeletedTaxRateResponse = await request.get(
@@ -247,14 +240,15 @@ test.describe.serial( 'Tax Rates API tests: CRUD', () => {
expect( deletedTaxRateIds ).toEqual( taxRateIdsToDelete );
// only run this step on wp-env -- caching with external hosting makes unreliable
+ // eslint-disable-next-line playwright/no-conditional-in-test
if ( ! shouldSkip ) {
// Verify that the deleted tax rates cannot be retrieved.
- for ( const taxRateId of taxRateIdsToDelete ) {
- //Call the API to attempte to retrieve the tax rates
- const response = await request.get(
- `wp-json/wc/v3/taxes/${ taxRateId }`
+ for ( const id of taxRateIdsToDelete ) {
+ // Call the API to attempt to retrieve the tax rates
+ const r = await request.get(
+ `wp-json/wc/v3/taxes/${ id }`
);
- expect( response.status() ).toEqual( 404 );
+ expect( r.status() ).toEqual( 404 );
}
}
} );
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/webhooks/webhooks-crud.test.js
similarity index 97%
rename from plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js
rename to plugins/woocommerce/tests/e2e-pw/tests/api-tests/webhooks/webhooks-crud.test.js
index dfb84b2a623..928c7cbd6dd 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/webhooks/webhooks-crud.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/webhooks/webhooks-crud.test.js
@@ -1,12 +1,5 @@
-const { test, expect } = require( '@playwright/test' );
+const { test, expect } = require( '../../../fixtures/api-tests-fixtures' );
-/**
- * Tests for the WooCommerce Refunds API.
- *
- * @group api
- * @group webhooks
- *
- */
test.describe( 'Webhooks API tests', () => {
let webhookId;
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/basic.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/basic.spec.js
index 255a23d738d..4c164d7fec1 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/basic.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/basic.spec.js
@@ -9,9 +9,7 @@ test( 'Load the home page', async ( { page } ) => {
.getByRole( 'link', { name: 'WooCommerce Core E2E Test' } )
.count()
).toBeGreaterThan( 0 );
- await expect(
- page.getByText( 'Proudly powered by WordPress' )
- ).toBeVisible();
+ await expect( page.getByText( /powered by WordPress/i ) ).toBeVisible();
expect( await page.title() ).toBe( 'WooCommerce Core E2E Test Suite' );
await expect(
page.getByRole( 'link', { name: 'WordPress' } )
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/color-picker.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/color-picker.spec.js
index 06e5f941e39..6a5a91e646b 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/color-picker.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/color-picker.spec.js
@@ -4,6 +4,7 @@ const { CustomizeStorePage } = require( '../customize-store.page' );
const { encodeCredentials } = require( '../../../utils/plugin-utils' );
const { activateTheme, DEFAULT_THEME } = require( '../../../utils/themes' );
+const { getInstalledWordPressVersion } = require( '../../../utils/wordpress' );
const { setOption } = require( '../../../utils/options' );
const test = base.extend( {
@@ -434,17 +435,22 @@ test.describe( 'Assembler -> Color Pickers', { tag: '@gutenberg' }, () => {
response.status() === 200
);
- const waitResponseTemplate = page.waitForResponse(
- ( response ) =>
- response.url().includes(
- // When CYS will support all block themes, this URL will change.
- 'wp-json/wp/v2/templates/twentytwentyfour//home'
- ) && response.status() === 200
- );
+ const wordPressVersion = await getInstalledWordPressVersion();
await saveButton.click();
- await Promise.all( [ waitResponseGlobalStyles, waitResponseTemplate ] );
+ await Promise.all( [
+ waitResponseGlobalStyles,
+ wordPressVersion < 6.6
+ ? page.waitForResponse(
+ ( response ) =>
+ response.url().includes(
+ // When CYS will support all block themes, this URL will change.
+ 'wp-json/wp/v2/templates/twentytwentyfour//home'
+ ) && response.status() === 200
+ )
+ : Promise.resolve(),
+ ] );
await page.goto( baseURL );
@@ -510,6 +516,8 @@ test.describe( 'Assembler -> Color Pickers', { tag: '@gutenberg' }, () => {
baseURL,
}, testInfo ) => {
testInfo.snapshotSuffix = '';
+ const wordPressVersion = await getInstalledWordPressVersion();
+
const assembler = await assemblerPageObject.getAssembler();
const colorPicker = assembler.getByText( 'Create your own' );
@@ -585,7 +593,7 @@ test.describe( 'Assembler -> Color Pickers', { tag: '@gutenberg' }, () => {
.click();
// eslint-disable-next-line playwright/no-conditional-in-test
- if ( gutenbergPlugin ) {
+ if ( gutenbergPlugin || wordPressVersion >= 6.6 ) {
for ( const feature of mapTypeFeaturesGutenberg[ type ] ) {
const container = assembler.locator(
'.block-editor-panel-color-gradient-settings__dropdown-content'
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/font-picker.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/font-picker.spec.js
index c467d4f0d05..079288d886b 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/font-picker.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/font-picker.spec.js
@@ -57,6 +57,13 @@ test.describe( 'Assembler -> Font Picker', { tag: '@gutenberg' }, () => {
'woocommerce_customize_store_onboarding_tour_hidden',
'yes'
);
+
+ await setOption(
+ request,
+ baseURL,
+ 'woocommerce_allow_tracking',
+ 'no'
+ );
} catch ( error ) {
console.log( 'Store completed option not updated' );
}
@@ -78,6 +85,13 @@ test.describe( 'Assembler -> Font Picker', { tag: '@gutenberg' }, () => {
'no'
);
+ await setOption(
+ request,
+ baseURL,
+ 'woocommerce_allow_tracking',
+ 'no'
+ );
+
// Reset theme back to default.
await activateTheme( DEFAULT_THEME );
} catch ( error ) {
@@ -209,4 +223,26 @@ test.describe( 'Assembler -> Font Picker', { tag: '@gutenberg' }, () => {
expect( isPrimaryFontUsed ).toBe( true );
expect( isSecondaryFontUsed ).toBe( true );
} );
+
+ test( 'Clicking opt-in new fonts should be available', async ( {
+ pageObject,
+ } ) => {
+ const assembler = await pageObject.getAssembler();
+
+ await assembler.getByText( 'Usage tracking' ).click();
+ await expect(
+ assembler.getByText( 'Access more fonts' )
+ ).toBeVisible();
+
+ await assembler.getByRole( 'button', { name: 'Opt in' } ).click();
+
+ await assembler
+ .getByText( 'Access more fonts' )
+ .waitFor( { state: 'hidden' } );
+
+ const fontPickers = assembler.locator(
+ '.woocommerce-customize-store_global-styles-variations_item'
+ );
+ await expect( fontPickers ).toHaveCount( 10 );
+ } );
} );
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/full-composability.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/full-composability.spec.js
new file mode 100644
index 00000000000..3130b77023f
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/full-composability.spec.js
@@ -0,0 +1,346 @@
+const { test: base, expect, request } = require( '@playwright/test' );
+const { AssemblerPage } = require( './assembler.page' );
+const { activateTheme, DEFAULT_THEME } = require( '../../../utils/themes' );
+const { setOption } = require( '../../../utils/options' );
+const { getInstalledWordPressVersion } = require( '../../../utils/wordpress' );
+
+const test = base.extend( {
+ pageObject: async ( { page }, use ) => {
+ const pageObject = new AssemblerPage( { page } );
+ await use( pageObject );
+ },
+} );
+
+async function prepareAssembler( pageObject, baseURL ) {
+ await pageObject.setupSite( baseURL );
+ await pageObject.waitForLoadingScreenFinish();
+ const assembler = await pageObject.getAssembler();
+ await assembler.getByText( 'Design your homepage' ).click();
+ await assembler
+ .locator( '.components-placeholder__preview' )
+ .waitFor( { state: 'hidden' } );
+}
+
+async function deleteAllPatterns( editor, assembler ) {
+ const previewPatterns = await editor
+ .locator(
+ '[data-is-parent-block="true"]:not([data-type="core/template-part"])'
+ )
+ .all();
+
+ for ( const pattern of previewPatterns ) {
+ await pattern.click();
+ const deleteButton = assembler.locator( 'button[aria-label="Delete"]' );
+ await deleteButton.click();
+ }
+}
+
+test.describe( 'Assembler -> Full composability', { tag: '@gutenberg' }, () => {
+ test.use( { storageState: process.env.ADMINSTATE } );
+
+ test.beforeAll( async ( { baseURL } ) => {
+ try {
+ // In some environments the tour blocks clicking other elements.
+ await setOption(
+ request,
+ baseURL,
+ 'woocommerce_customize_store_onboarding_tour_hidden',
+ 'yes'
+ );
+
+ await setOption(
+ request,
+ baseURL,
+ 'woocommerce_allow_tracking',
+ 'no'
+ );
+ } catch ( error ) {
+ console.log( 'Store completed option not updated' );
+ }
+
+ const wordPressVersion = await getInstalledWordPressVersion();
+
+ if ( wordPressVersion <= 6.5 ) {
+ test.skip(
+ 'Skipping Full Composability tests: WordPress version is below 6.5, which does not support this feature.'
+ );
+ }
+ } );
+
+ test.afterAll( async ( { baseURL } ) => {
+ try {
+ // In some environments the tour blocks clicking other elements.
+ await setOption(
+ request,
+ baseURL,
+ 'woocommerce_customize_store_onboarding_tour_hidden',
+ 'no'
+ );
+ await setOption(
+ request,
+ baseURL,
+ 'woocommerce_admin_customize_store_completed',
+ 'no'
+ );
+
+ await setOption(
+ request,
+ baseURL,
+ 'woocommerce_allow_tracking',
+ 'no'
+ );
+
+ await activateTheme( DEFAULT_THEME );
+ } catch ( error ) {
+ console.log( 'Store completed option not updated' );
+ }
+ } );
+
+ test( 'The list of categories should be displayed', async ( {
+ pageObject,
+ baseURL,
+ } ) => {
+ await prepareAssembler( pageObject, baseURL );
+ const assembler = await pageObject.getAssembler();
+
+ const categories = assembler.locator(
+ '.woocommerce-customize-store__sidebar-homepage-content .components-item-group'
+ );
+
+ await expect( categories ).toHaveCount( 6 );
+ } );
+
+ test( 'Clicking on "Design your homepage" should open the Intro sidebar by default', async ( {
+ pageObject,
+ baseURL,
+ } ) => {
+ await prepareAssembler( pageObject, baseURL );
+ const assembler = await pageObject.getAssembler();
+
+ await expect(
+ await assembler
+ .locator(
+ '.woocommerce-customize-store-edit-site-layout__sidebar-extra__pattern__header'
+ )
+ .textContent()
+ ).toContain( 'Intro' );
+ } );
+
+ test( 'Clicking on a category should open the sidebar for it', async ( {
+ pageObject,
+ baseURL,
+ } ) => {
+ await prepareAssembler( pageObject, baseURL );
+ const assembler = await pageObject.getAssembler();
+
+ const categories = await assembler
+ .locator(
+ '.edit-site-sidebar-navigation-screen-patterns__group-homepage-label-container > span:first-child'
+ )
+ .all();
+
+ for ( const category of categories ) {
+ const name = await category.textContent();
+ await category.click();
+
+ const sidebar = assembler.locator(
+ '.woocommerce-customize-store-edit-site-layout__sidebar-extra'
+ );
+ await expect( sidebar ).toBeVisible();
+
+ const sidebarTitle = await sidebar
+ .locator(
+ '.woocommerce-customize-store-edit-site-layout__sidebar-extra__pattern__header'
+ )
+ .textContent();
+
+ await expect( sidebarTitle ).toBe( name );
+ await expect( async () => {
+ const count = await sidebar
+ .locator( '.block-editor-block-patterns-list__list-item' )
+ .count();
+ expect( count ).toBeGreaterThan( 0 );
+ } ).toPass();
+ }
+ } );
+
+ test( 'Clicking on a pattern should insert it in the preview', async ( {
+ pageObject,
+ baseURL,
+ } ) => {
+ await prepareAssembler( pageObject, baseURL );
+ const assembler = await pageObject.getAssembler();
+ const editor = await pageObject.getEditor();
+
+ await deleteAllPatterns( editor, assembler );
+
+ const sidebarPattern = assembler
+ .locator( '.block-editor-block-patterns-list__list-item' )
+ .first();
+
+ const sidebarPatternContent = await sidebarPattern
+ .frameLocator( 'iframe' )
+ .locator( '.is-root-container' )
+ .textContent();
+
+ await sidebarPattern.click();
+
+ const insertedPatternContent = await editor
+ .locator(
+ '[data-is-parent-block="true"]:not([data-type="core/template-part"])'
+ )
+ .first()
+ .textContent();
+
+ await expect( insertedPatternContent ).toContain(
+ sidebarPatternContent
+ );
+ } );
+
+ test( 'Clicking the "Move up/down" buttons should change the pattern order in the preview', async ( {
+ pageObject,
+ baseURL,
+ } ) => {
+ await prepareAssembler( pageObject, baseURL );
+ const assembler = await pageObject.getAssembler();
+ const editor = await pageObject.getEditor();
+
+ const sidebarPattern = assembler
+ .locator( '.block-editor-block-patterns-list__list-item' )
+ .nth( 2 );
+
+ const sidebarPatternContent = await sidebarPattern
+ .frameLocator( 'iframe' )
+ .locator( '.is-root-container' )
+ .textContent();
+
+ await sidebarPattern.click();
+
+ const insertedPattern = editor
+ .locator(
+ '[data-is-parent-block="true"]:not([data-type="core/template-part"])'
+ )
+ .nth( 1 );
+ await insertedPattern.click();
+
+ const moveUpButton = assembler.locator(
+ 'button[aria-label="Move up"]'
+ );
+ await moveUpButton.click();
+
+ const firstPattern = editor
+ .locator(
+ '[data-is-parent-block="true"]:not([data-type="core/template-part"])'
+ )
+ .first();
+ const firstPatternContent = await firstPattern.textContent();
+
+ expect( firstPatternContent ).toContain( sidebarPatternContent );
+ } );
+
+ test( 'Clicking the "Shuffle" button on a patterns should replace it for another one', async ( {
+ pageObject,
+ baseURL,
+ } ) => {
+ await prepareAssembler( pageObject, baseURL );
+ const assembler = await pageObject.getAssembler();
+ const editor = await pageObject.getEditor();
+
+ await deleteAllPatterns( editor, assembler );
+
+ const sidebarPattern = assembler
+ .locator( '.block-editor-block-patterns-list__list-item' )
+ .first();
+ await sidebarPattern.click();
+
+ const insertedPattern = editor
+ .locator(
+ '[data-is-parent-block="true"]:not([data-type="core/template-part"])'
+ )
+ .first();
+
+ const insertedPatternContent = await insertedPattern.textContent();
+
+ await insertedPattern.click();
+ const shuffleButton = assembler.locator(
+ 'button[aria-label="Shuffle"]'
+ );
+ await shuffleButton.click();
+
+ const shuffledPattern = editor.locator(
+ '[data-is-parent-block="true"]:not([data-type="core/template-part"])'
+ );
+
+ await expect( await shuffledPattern ).not.toHaveText(
+ insertedPatternContent
+ );
+ } );
+
+ test( 'Clicking the "Delete" button on a pattern should remove it from the preview', async ( {
+ pageObject,
+ baseURL,
+ } ) => {
+ await prepareAssembler( pageObject, baseURL );
+ const assembler = await pageObject.getAssembler();
+ const editor = await pageObject.getEditor();
+
+ await deleteAllPatterns( editor, assembler );
+
+ const emptyPatternsBlock = editor.getByText(
+ 'Add one or more of our homepage patterns to create a page that welcomes shoppers.'
+ );
+ await expect( emptyPatternsBlock ).toBeVisible();
+ } );
+
+ test( 'Clicking the "Add patterns" button on the No Blocks view should add a default pattern', async ( {
+ pageObject,
+ baseURL,
+ } ) => {
+ await prepareAssembler( pageObject, baseURL );
+ const assembler = await pageObject.getAssembler();
+ const editor = await pageObject.getEditor();
+
+ await deleteAllPatterns( editor, assembler );
+ const addPatternsButton = editor.locator(
+ '.no-blocks-insert-pattern-button'
+ );
+ await addPatternsButton.click();
+ const emptyPatternsBlock = editor.getByText(
+ 'Add one or more of our homepage patterns to create a page that welcomes shoppers.'
+ );
+ const defaultPattern = editor.locator(
+ '[data-is-parent-block="true"]:not([data-type="core/template-part"])'
+ );
+ await expect( emptyPatternsBlock ).toBeHidden();
+ await expect( defaultPattern ).toBeVisible();
+ } );
+
+ test( 'Clicking opt-in new patterns should be available', async ( {
+ pageObject,
+ baseURL,
+ } ) => {
+ await prepareAssembler( pageObject, baseURL );
+ const assembler = await pageObject.getAssembler();
+
+ await assembler.getByText( 'Usage tracking' ).click();
+ await expect(
+ assembler.getByText( 'Access more patterns' )
+ ).toBeVisible();
+
+ await assembler.getByRole( 'button', { name: 'Opt in' } ).click();
+
+ await assembler
+ .getByText( 'Access more patterns' )
+ .waitFor( { state: 'hidden' } );
+
+ const sidebarPattern = assembler.locator(
+ '.block-editor-block-patterns-list'
+ );
+
+ await sidebarPattern.waitFor( { state: 'visible' } );
+
+ await expect(
+ assembler.locator( '.block-editor-block-patterns-list__list-item' )
+ ).toHaveCount( 10 );
+ } );
+} );
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js
index bd295031fba..66239125734 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js
@@ -1,7 +1,9 @@
const { test: base, expect, request } = require( '@playwright/test' );
const { AssemblerPage } = require( './assembler.page' );
const { activateTheme, DEFAULT_THEME } = require( '../../../utils/themes' );
+const { getInstalledWordPressVersion } = require( '../../../utils/wordpress' );
const { setOption } = require( '../../../utils/options' );
+const { encodeCredentials } = require( '../../../utils/plugin-utils' );
const test = base.extend( {
pageObject: async ( { page }, use ) => {
@@ -35,6 +37,14 @@ test.describe( 'Assembler -> Homepage', { tag: '@gutenberg' }, () => {
} catch ( error ) {
console.log( 'Store completed option not updated' );
}
+
+ const wordPressVersion = await getInstalledWordPressVersion();
+
+ if ( wordPressVersion > 6.5 ) {
+ test.skip(
+ 'Skipping Assembler Homepage tests: WordPress version is above 6.5, which does not support this feature.'
+ );
+ }
} );
test.afterAll( async ( { baseURL } ) => {
@@ -163,146 +173,162 @@ test.describe( 'Assembler -> Homepage', { tag: '@gutenberg' }, () => {
await waitResponse;
await page.goto( baseURL );
- // Get all the content between the header and the footer.
- const homepageHTML = await page
- .locator(
- '//header/following-sibling::*[following-sibling::footer]'
- )
- .all();
- let index = 0;
- for ( const element of homepageHTML ) {
- await expect(
- await element.getAttribute( 'class' )
- ).toMatchSnapshot( {
- name: `selected-homepage-blocks-class-frontend-${ index }`,
- } );
- index++;
+ // Check if Gutenberg is installed
+ const apiContext = await request.newContext( {
+ baseURL,
+ extraHTTPHeaders: {
+ Authorization: `Basic ${ encodeCredentials(
+ 'admin',
+ 'password'
+ ) }`,
+ cookie: '',
+ },
+ } );
+ const listPluginsResponse = await apiContext.get(
+ `/wp-json/wp/v2/plugins`,
+ {
+ failOnStatusCode: true,
+ }
+ );
+ const pluginsList = await listPluginsResponse.json();
+ const withGutenbergPlugin = pluginsList.find(
+ ( { textdomain } ) => textdomain === 'gutenberg'
+ );
+
+ // if testing with Gutenberg, perform Gutenberg-specific testing
+ // eslint-disable-next-line playwright/no-conditional-in-test
+ if ( withGutenbergPlugin ) {
+ // Get all the content between the header and the footer.
+ const homepageHTML = await page
+ .locator(
+ '//header/following-sibling::*[following-sibling::footer]'
+ )
+ .all();
+
+ let index = 0;
+ for ( const element of homepageHTML ) {
+ await expect(
+ await element.getAttribute( 'class' )
+ ).toMatchSnapshot( {
+ name: `${
+ withGutenbergPlugin ? 'gutenberg' : ''
+ }-selected-homepage-blocks-class-frontend-${ index }`,
+ } );
+ index++;
+ }
}
} );
-
- test.describe( 'Homepage tracking banner', () => {
- test( 'Should show the "Want more patterns?" banner with the Opt-in message when tracking is not allowed', async ( {
- pageObject,
- baseURL,
- } ) => {
- await setOption(
- request,
- baseURL,
- 'woocommerce_allow_tracking',
- 'no'
- );
-
- await prepareAssembler( pageObject, baseURL );
-
- const assembler = await pageObject.getAssembler();
- await expect(
- assembler.getByText( 'Want more patterns?' )
- ).toBeVisible();
- await expect(
- assembler.getByText(
- 'Opt in to usage tracking to get access to more patterns.'
- )
- ).toBeVisible();
- } );
-
- test( 'Should show the "Want more patterns?" banner with the offline message when the user is offline and tracking is not allowed', async ( {
- context,
- pageObject,
- baseURL,
- } ) => {
- await setOption(
- request,
- baseURL,
- 'woocommerce_allow_tracking',
- 'no'
- );
-
- await prepareAssembler( pageObject, baseURL );
-
- await context.setOffline( true );
-
- const assembler = await pageObject.getAssembler();
- await expect(
- assembler.getByText( 'Want more patterns?' )
- ).toBeVisible();
- await expect(
- assembler.getByText(
- "Looks like we can't detect your network. Please double-check your internet connection and refresh the page."
- )
- ).toBeVisible();
- } );
-
- test( 'Should not show the "Want more patterns?" banner when tracking is allowed', async ( {
- baseURL,
- pageObject,
- } ) => {
- await setOption(
- request,
- baseURL,
- 'woocommerce_allow_tracking',
- 'yes'
- );
-
- await prepareAssembler( pageObject, baseURL );
-
- const assembler = await pageObject.getAssembler();
- await expect(
- assembler.getByText( 'Want more patterns?' )
- ).toBeHidden();
- } );
- } );
} );
-test.describe(
- 'Assembler -> Homepage -> PTK API is down',
- { tag: '@gutenberg' },
- () => {
- test.use( { storageState: process.env.ADMINSTATE } );
+test.describe( 'Homepage tracking banner', () => {
+ test.use( { storageState: process.env.ADMINSTATE } );
- test.beforeAll( async ( { baseURL } ) => {
- try {
- // In some environments the tour blocks clicking other elements.
- await setOption(
- request,
- baseURL,
- 'woocommerce_customize_store_onboarding_tour_hidden',
- 'yes'
- );
- } catch ( error ) {
- console.log( 'Store completed option not updated' );
- }
- } );
-
- test( 'Should show the "Want more patterns?" banner with the PTK API unavailable message', async ( {
- baseURL,
- pageObject,
- page,
- } ) => {
+ test.beforeAll( async ( { baseURL } ) => {
+ try {
+ // In some environments the tour blocks clicking other elements.
await setOption(
request,
baseURL,
- 'woocommerce_allow_tracking',
- 'no'
+ 'woocommerce_customize_store_onboarding_tour_hidden',
+ 'yes'
);
+ } catch ( error ) {
+ console.log( 'Store completed option not updated' );
+ }
- await page.route( '**/wp-json/wc/private/patterns*', ( route ) => {
- route.fulfill( {
- status: 500,
- } );
+ const wordPressVersion = await getInstalledWordPressVersion();
+
+ if ( wordPressVersion <= 6.5 ) {
+ test.skip(
+ 'Skipping PTK API test: WordPress version is below 6.5, which does not support this feature.'
+ );
+ }
+ } );
+
+ test( 'Should show the "Want more patterns?" banner with the PTK API unavailable message', async ( {
+ baseURL,
+ pageObject,
+ page,
+ } ) => {
+ await setOption( request, baseURL, 'woocommerce_allow_tracking', 'no' );
+
+ await page.route( '**/wp-json/wc/private/patterns*', ( route ) => {
+ route.fulfill( {
+ status: 500,
} );
-
- await prepareAssembler( pageObject, baseURL );
-
- const assembler = await pageObject.getAssembler();
- await expect(
- assembler.getByText( 'Want more patterns?' )
- ).toBeVisible();
- await expect(
- assembler.getByText(
- "Unfortunately, we're experiencing some technical issues — please come back later to access more patterns."
- )
- ).toBeVisible();
} );
- }
-);
+
+ await prepareAssembler( pageObject, baseURL );
+
+ const assembler = await pageObject.getAssembler();
+ await expect(
+ assembler.getByText( 'Want more patterns?' )
+ ).toBeVisible();
+ await expect(
+ assembler.getByText(
+ "Unfortunately, we're experiencing some technical issues — please come back later to access more patterns."
+ )
+ ).toBeVisible();
+ } );
+
+ test( 'Should show the "Want more patterns?" banner with the Opt-in message when tracking is not allowed', async ( {
+ pageObject,
+ baseURL,
+ } ) => {
+ await setOption( request, baseURL, 'woocommerce_allow_tracking', 'no' );
+
+ await prepareAssembler( pageObject, baseURL );
+
+ const assembler = await pageObject.getAssembler();
+ await expect(
+ assembler.getByText( 'Want more patterns?' )
+ ).toBeVisible();
+ await expect(
+ assembler.getByText(
+ 'Opt in to usage tracking to get access to more patterns.'
+ )
+ ).toBeVisible();
+ } );
+
+ test( 'Should show the "Want more patterns?" banner with the offline message when the user is offline and tracking is not allowed', async ( {
+ context,
+ pageObject,
+ baseURL,
+ } ) => {
+ await setOption( request, baseURL, 'woocommerce_allow_tracking', 'no' );
+
+ await prepareAssembler( pageObject, baseURL );
+
+ await context.setOffline( true );
+
+ const assembler = await pageObject.getAssembler();
+ await expect(
+ assembler.getByText( 'Want more patterns?' )
+ ).toBeVisible();
+ await expect(
+ assembler.getByText(
+ "Looks like we can't detect your network. Please double-check your internet connection and refresh the page."
+ )
+ ).toBeVisible();
+ } );
+
+ test( 'Should not show the "Want more patterns?" banner when tracking is allowed', async ( {
+ baseURL,
+ pageObject,
+ } ) => {
+ await setOption(
+ request,
+ baseURL,
+ 'woocommerce_allow_tracking',
+ 'yes'
+ );
+
+ await prepareAssembler( pageObject, baseURL );
+
+ const assembler = await pageObject.getAssembler();
+ await expect(
+ assembler.getByText( 'Want more patterns?' )
+ ).toBeHidden();
+ } );
+} );
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js-snapshots/gutenberg-selected-homepage-blocks-class-frontend-0 b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js-snapshots/gutenberg-selected-homepage-blocks-class-frontend-0
new file mode 100644
index 00000000000..6e8d1489d3a
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js-snapshots/gutenberg-selected-homepage-blocks-class-frontend-0
@@ -0,0 +1 @@
+wp-block-group alignfull has-global-padding is-content-justification-center is-layout-constrained wp-container-core-group-is-layout-5 wp-block-group-is-layout-constrained
\ No newline at end of file
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js-snapshots/gutenberg-selected-homepage-blocks-class-frontend-1 b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js-snapshots/gutenberg-selected-homepage-blocks-class-frontend-1
new file mode 100644
index 00000000000..45e5bab5d6a
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js-snapshots/gutenberg-selected-homepage-blocks-class-frontend-1
@@ -0,0 +1 @@
+wp-block-group alignfull has-global-padding is-content-justification-center is-layout-constrained wp-container-core-group-is-layout-7 wp-block-group-is-layout-constrained
\ No newline at end of file
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js-snapshots/gutenberg-selected-homepage-blocks-class-frontend-2 b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js-snapshots/gutenberg-selected-homepage-blocks-class-frontend-2
new file mode 100644
index 00000000000..9b5f023055b
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js-snapshots/gutenberg-selected-homepage-blocks-class-frontend-2
@@ -0,0 +1 @@
+wp-block-group alignfull has-global-padding is-content-justification-center is-layout-constrained wp-container-core-group-is-layout-8 wp-block-group-is-layout-constrained
\ No newline at end of file
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js-snapshots/gutenberg-selected-homepage-blocks-class-frontend-3 b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js-snapshots/gutenberg-selected-homepage-blocks-class-frontend-3
new file mode 100644
index 00000000000..9255472358e
--- /dev/null
+++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/homepage.spec.js-snapshots/gutenberg-selected-homepage-blocks-class-frontend-3
@@ -0,0 +1 @@
+wp-block-group alignfull has-global-padding is-content-justification-center is-layout-constrained wp-container-core-group-is-layout-9 wp-block-group-is-layout-constrained
\ No newline at end of file
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/logo-picker/logo-picker.page.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/logo-picker/logo-picker.page.js
index 07bc9f9e1ae..0fb8a38542f 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/logo-picker/logo-picker.page.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/logo-picker/logo-picker.page.js
@@ -49,6 +49,10 @@ export class LogoPickerPage {
.click();
}
+ getPlaceholderPreview( assemblerLocator ) {
+ return assemblerLocator.locator( '.components-placeholder__preview' );
+ }
+
async resetLogo( baseURL ) {
const apiContext = await this.request.newContext( {
baseURL,
@@ -69,18 +73,19 @@ export class LogoPickerPage {
}
async saveLogoSettings( assemblerLocator ) {
- await assemblerLocator.locator( '[aria-label="Back"]' ).click();
const waitForLogoResponse = this.page.waitForResponse(
( response ) =>
response.url().includes( 'wp-json/wp/v2/settings' ) &&
response.status() === 200
);
- const waitForHeaderResponse = this.page.waitForResponse(
- ( response ) =>
- response.url().includes( '//header' ) &&
- response.status() === 200
- );
- await assemblerLocator.getByText( 'Save' ).click();
- await Promise.all( [ waitForLogoResponse, waitForHeaderResponse ] );
+ await assemblerLocator.locator( '[aria-label="Back"]' ).click();
+ await assemblerLocator
+ .getByRole( 'button', { name: 'Save', exact: true } )
+ .waitFor();
+ await Promise.all( [
+ waitForLogoResponse,
+ assemblerLocator.getByText( 'Save' ).click(),
+ ] );
+ await assemblerLocator.getByText( 'Your store looks great!' ).waitFor();
}
}
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/logo-picker/logo-picker.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/logo-picker/logo-picker.spec.js
index 65de08d2d83..979a3f1f9a1 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/logo-picker/logo-picker.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler/logo-picker/logo-picker.spec.js
@@ -142,12 +142,13 @@ test.describe( 'Assembler -> Logo Picker', { tag: '@gutenberg' }, () => {
await assembler
.getByRole( 'spinbutton', { name: 'Image width' } )
.fill( '100' );
- const { width } = await editor
- .getByLabel( 'Block: Header' )
- .getByLabel( 'Block: Site Logo' )
- .boundingBox();
- expect( Math.floor( width ) ).toBe( 100 );
+ await expect(
+ editor
+ .getByLabel( 'Block: Header' )
+ .getByLabel( 'Block: Site Logo' )
+ ).toHaveCSS( 'width', '100px' );
+
await logoPickerPageObject.saveLogoSettings( assembler );
const imageFrontend = await logoPickerPageObject.getLogoLocator( page );
await page.goto( baseURL );
@@ -186,10 +187,42 @@ test.describe( 'Assembler -> Logo Picker', { tag: '@gutenberg' }, () => {
await expect( assembler.getByText( 'Media Library' ) ).toBeVisible();
} );
- test( 'Enabling the "use as site icon" option should set the image as the site icon', async ( {
+ // This test checks this regression: https://github.com/woocommerce/woocommerce/issues/49668
+ test( 'Logo should be visible after header update', async ( {
assemblerPageObject,
logoPickerPageObject,
+ } ) => {
+ const assembler = await assemblerPageObject.getAssembler();
+ const emptyLogoPicker =
+ logoPickerPageObject.getEmptyLogoPickerLocator( assembler );
+ await emptyLogoPicker.click();
+ await logoPickerPageObject.pickImage( assembler );
+
+ await assembler.getByLabel( 'Back' ).click();
+
+ await assembler.getByText( 'Choose your header' ).click();
+
+ const header = assembler
+ .locator( '.block-editor-block-patterns-list__list-item' )
+ .nth( 1 )
+ .frameLocator( 'iframe' )
+ .locator( '.wc-blocks-header-pattern' );
+
+ await header.click();
+
+ await assembler.getByLabel( 'Back' ).click();
+
+ await assembler.getByText( 'Add your logo' ).click();
+ const emptyLogoLocator =
+ logoPickerPageObject.getPlaceholderPreview( assembler );
+
+ await expect( emptyLogoLocator ).toBeHidden();
+ } );
+
+ test( 'Enabling the "use as site icon" option should set the image as the site icon', async ( {
page,
+ assemblerPageObject,
+ logoPickerPageObject,
} ) => {
const assembler = await assemblerPageObject.getAssembler();
const emptyLogoPicker =
@@ -197,19 +230,16 @@ test.describe( 'Assembler -> Logo Picker', { tag: '@gutenberg' }, () => {
await emptyLogoPicker.click();
await logoPickerPageObject.pickImage( assembler );
await assembler.getByText( 'Use as site icon' ).click();
-
- let isAValidResponse = false;
- const waitForResponse = page.waitForResponse( ( response ) => {
- isAValidResponse =
- response.url().includes( '/wp-json/wp/v2/settings' ) &&
- response.request().method() === 'POST' &&
- response.status() === 200;
- return isAValidResponse;
- } );
await logoPickerPageObject.saveLogoSettings( assembler );
- await waitForResponse;
- await expect( isAValidResponse ).toBe( true );
+ // alternative way to verify new site icon on the site
+ // verifying site icon shown in the new tab is impossible in headless mode
+ const date = new Date();
+ await expect(
+ page.goto(
+ `/wp-content/uploads/${ date.getFullYear() }/${ date.getMonth() }/image-03-100x100.png`
+ )
+ ).toBeTruthy();
} );
test( 'The selected image should be visible on the frontend', async ( {
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/intro.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/intro.spec.js
index e4528cfb65d..449add25331 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/intro.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/intro.spec.js
@@ -111,49 +111,34 @@ test.describe(
'Customize your theme'
);
await expect(
- page.getByRole( 'button', { name: 'Customize your theme' } )
+ page.getByRole( 'button', { name: 'Customize your store' } )
).toBeVisible();
} );
- test( 'Clicking on "Customize your theme" with a block theme should go to the assembler', async ( {
+ test( 'it shows the "non default block theme" banner when the theme is a block theme different than TT4', async ( {
page,
- assemblerPageObject,
} ) => {
- await page.goto( CUSTOMIZE_STORE_URL );
- await page.click( 'text=Start designing' );
- await assemblerPageObject.waitForLoadingScreenFinish();
+ await activateTheme( 'twentytwentythree' );
await page.goto( CUSTOMIZE_STORE_URL );
- await page
- .getByRole( 'button', { name: 'Customize your theme' } )
- .click();
- const assembler = await assemblerPageObject.getAssembler();
+ await expect( page.locator( 'h1' ) ).toHaveText(
+ 'Customize your theme'
+ );
await expect(
- assembler.locator( "text=Let's get creative" )
+ page.getByRole( 'button', { name: 'Go to the Editor' } )
).toBeVisible();
} );
- test( 'clicking on "Customize your theme" with a classic theme should go to the customizer', async ( {
+ test( 'clicking on "Go to the Customizer" with a classic theme should go to the customizer', async ( {
page,
- baseURL,
} ) => {
await activateTheme( 'twentytwenty' );
- try {
- await setOption(
- request,
- baseURL,
- 'woocommerce_admin_customize_store_completed',
- 'yes'
- );
- } catch ( error ) {
- console.log( 'Store completed option not updated' );
- }
await page.goto( CUSTOMIZE_STORE_URL );
await page
- .getByRole( 'button', { name: 'Customize your theme' } )
+ .getByRole( 'button', { name: 'Go to the Customizer' } )
.click();
await page.waitForNavigation();
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/command-palette.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/command-palette.spec.js
index ef0525ce4ee..bf5502915ff 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/command-palette.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/command-palette.spec.js
@@ -9,9 +9,11 @@ const clickOnCommandPaletteOption = async ( { page, optionName } ) => {
// Press `Ctrl` + `K` to open the command palette.
await page.keyboard.press( cmdKeyCombo );
+ // Using a regex here because Gutenberg changes the text of the placeholder
await page
- .getByLabel( 'Command palette' )
- .locator( 'input' )
+ .getByPlaceholder(
+ /Search (?:commands(?: and settings)?|for commands)/
+ )
.fill( optionName );
// Click on the relevant option.
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-cart-block.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-cart-block.spec.js
index f175005f5ba..b262e740846 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-cart-block.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-cart-block.spec.js
@@ -6,6 +6,7 @@ const {
transformIntoBlocks,
publishPage,
} = require( '../../utils/editor' );
+const { getInstalledWordPressVersion } = require( '../../utils/wordpress' );
const test = baseTest.extend( {
storageState: process.env.ADMINSTATE,
@@ -23,7 +24,8 @@ test.describe(
await goToPageEditor( { page } );
await fillPageTitle( page, testPage.title );
- await insertBlock( page, 'Classic Cart' );
+ const wordPressVersion = await getInstalledWordPressVersion();
+ await insertBlock( page, 'Classic Cart', wordPressVersion );
await transformIntoBlocks( page );
await publishPage( page, testPage.title );
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-checkout-block.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-checkout-block.spec.js
index 1ea6f72308b..f12eb0180d8 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-checkout-block.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-checkout-block.spec.js
@@ -8,6 +8,7 @@ const {
publishPage,
openEditorSettings,
} = require( '../../utils/editor' );
+const { getInstalledWordPressVersion } = require( '../../utils/wordpress' );
const simpleProductName = 'Very Simple Product';
const singleProductPrice = '999.00';
@@ -74,7 +75,8 @@ test.describe(
await goToPageEditor( { page } );
await fillPageTitle( page, testPage.title );
- await insertBlock( page, 'Classic Checkout' );
+ const wordPressVersion = await getInstalledWordPressVersion();
+ await insertBlock( page, 'Classic Checkout', wordPressVersion );
await transformIntoBlocks( page );
// When Gutenberg is active, the canvas is in an iframe
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-page.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-page.spec.js
index 46cb88ecf5a..f5fae88cfba 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-page.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-page.spec.js
@@ -4,6 +4,7 @@ const {
fillPageTitle,
getCanvas,
publishPage,
+ closeChoosePatternModal,
} = require( '../../utils/editor' );
const test = baseTest.extend( {
@@ -18,6 +19,8 @@ test.describe(
test( 'can create new page', async ( { page, testPage } ) => {
await goToPageEditor( { page } );
+ await closeChoosePatternModal( { page } );
+
await fillPageTitle( page, testPage.title );
const canvas = await getCanvas( page );
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-post.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-post.spec.js
index 238cf1aa8e1..b03e79fe5da 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-post.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-post.spec.js
@@ -14,7 +14,6 @@ test.describe(
'Can create a new post',
{ tag: [ '@gutenberg', '@services' ] },
() => {
- // eslint-disable-next-line playwright/expect-expect
test( 'can create new post', async ( { page, testPost } ) => {
await goToPostEditor( { page } );
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-blocks.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-blocks.spec.js
index e8ce23d6192..ed5768ae08e 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-blocks.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-blocks.spec.js
@@ -5,7 +5,9 @@ const {
insertBlock,
getCanvas,
publishPage,
+ closeChoosePatternModal,
} = require( '../../utils/editor' );
+const { getInstalledWordPressVersion } = require( '../../utils/wordpress' );
const simpleProductName = 'Simplest Product';
const singleProductPrice = '555.00';
@@ -146,11 +148,15 @@ test.describe(
} ) => {
await goToPageEditor( { page } );
+ await closeChoosePatternModal( { page } );
+
await fillPageTitle( page, testPage.title );
+ const wordPressVersion = await getInstalledWordPressVersion();
+
for ( let i = 0; i < blocks.length; i++ ) {
await test.step( `Insert ${ blocks[ i ] } block`, async () => {
- await insertBlock( page, blocks[ i ] );
+ await insertBlock( page, blocks[ i ], wordPressVersion );
const canvas = await getCanvas( page );
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-patterns.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-patterns.spec.js
index 133a7acaacd..27071e6cb29 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-patterns.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/create-woocommerce-patterns.spec.js
@@ -5,7 +5,9 @@ const {
insertBlock,
getCanvas,
publishPage,
+ closeChoosePatternModal,
} = require( '../../utils/editor' );
+const { getInstalledWordPressVersion } = require( '../../utils/wordpress' );
// some WooCommerce Patterns to use
const wooPatterns = [
@@ -33,11 +35,20 @@ test.describe(
testPage,
} ) => {
await goToPageEditor( { page } );
+
+ await closeChoosePatternModal( { page } );
+
await fillPageTitle( page, testPage.title );
+ const wordPressVersion = await getInstalledWordPressVersion();
+
for ( let i = 0; i < wooPatterns.length; i++ ) {
await test.step( `Insert ${ wooPatterns[ i ].name } pattern`, async () => {
- await insertBlock( page, wooPatterns[ i ].name );
+ await insertBlock(
+ page,
+ wooPatterns[ i ].name,
+ wordPressVersion
+ );
await expect(
page.getByLabel( 'Dismiss this notice' ).filter( {
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-list.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-list.spec.js
index ab19c146694..4a0fe825a78 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-list.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/customer-list.spec.js
@@ -128,7 +128,13 @@ test.describe( 'Merchant > Customer List', { tag: '@services' }, () => {
name: `All customers with names that include ${ customer.first_name } ${ customer.last_name }`,
exact: true,
} )
- .waitFor( { state: 'visible' } );
+ .waitFor();
+ await page
+ .getByRole( 'option', {
+ name: `${ customer.first_name } ${ customer.last_name }`,
+ exact: true,
+ } )
+ .waitFor();
await page
.getByRole( 'option', {
name: `All customers with names that include ${ customer.first_name } ${ customer.last_name }`,
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-bulk-edit.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-bulk-edit.spec.js
index 1f54bf46505..99b5a38b5d5 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-bulk-edit.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-bulk-edit.spec.js
@@ -72,26 +72,31 @@ test.describe( 'Bulk edit orders', { tag: '@services' }, () => {
page
.locator( `:is(#order-${ orderId1 }, #post-${ orderId1 })` )
.getByText( 'Processing' )
+ .nth( 1 )
).toBeVisible();
await expect(
page
.locator( `:is(#order-${ orderId2 }, #post-${ orderId2 })` )
.getByText( 'Processing' )
+ .nth( 1 )
).toBeVisible();
await expect(
page
.locator( `:is(#order-${ orderId3 }, #post-${ orderId3 })` )
.getByText( 'Processing' )
+ .nth( 1 )
).toBeVisible();
await expect(
page
.locator( `:is(#order-${ orderId4 }, #post-${ orderId4 })` )
.getByText( 'Processing' )
+ .nth( 1 )
).toBeVisible();
await expect(
page
.locator( `:is(#order-${ orderId5 }, #post-${ orderId5 })` )
.getByText( 'Processing' )
+ .nth( 1 )
).toBeVisible();
await page.locator( '#cb-select-all-1' ).click();
@@ -105,26 +110,31 @@ test.describe( 'Bulk edit orders', { tag: '@services' }, () => {
page
.locator( `:is(#order-${ orderId1 }, #post-${ orderId1 })` )
.getByText( 'Completed' )
+ .nth( 1 )
).toBeVisible();
await expect(
page
.locator( `:is(#order-${ orderId2 }, #post-${ orderId2 })` )
.getByText( 'Completed' )
+ .nth( 1 )
).toBeVisible();
await expect(
page
.locator( `:is(#order-${ orderId3 }, #post-${ orderId3 })` )
.getByText( 'Completed' )
+ .nth( 1 )
).toBeVisible();
await expect(
page
.locator( `:is(#order-${ orderId4 }, #post-${ orderId4 })` )
.getByText( 'Completed' )
+ .nth( 1 )
).toBeVisible();
await expect(
page
.locator( `:is(#order-${ orderId5 }, #post-${ orderId5 })` )
.getByText( 'Completed' )
+ .nth( 1 )
).toBeVisible();
} );
} );
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/block-editor/create-variable-product-block-editor.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/block-editor/create-variable-product-block-editor.spec.js
index e57a71eae64..2a0954e8817 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/block-editor/create-variable-product-block-editor.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/products/block-editor/create-variable-product-block-editor.spec.js
@@ -290,17 +290,17 @@ test.describe( 'Variations tab', { tag: '@gutenberg' }, () => {
.getByRole( 'tab', { name: 'General' } )
.click();
- await page.getByLabel( 'Regular price', { exact: true } ).click();
-
await page
.getByLabel( 'Regular price', { exact: true } )
.waitFor( { state: 'visible' } );
await waitResponse;
+ await page.getByLabel( 'Regular price', { exact: true } ).click();
+
await page
.getByLabel( 'Regular price', { exact: true } )
- .pressSequentially( '100' );
+ .fill( '100' );
await page
.locator( '.woocommerce-product-tabs' )
@@ -308,7 +308,7 @@ test.describe( 'Variations tab', { tag: '@gutenberg' }, () => {
.click();
await page
- .locator( '#inspector-input-control-2' )
+ .locator( '[name="woocommerce-product-sku"]' )
.fill( `product-sku-${ new Date().getTime().toString() }` );
await page
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/account-email-receiving.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/account-email-receiving.spec.js
index 2fdb181c745..8110a3aff5c 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/account-email-receiving.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/account-email-receiving.spec.js
@@ -226,6 +226,9 @@ test.describe(
test( 'should receive an email when initiating a password reset', async ( {
page,
} ) => {
+ // Effect a log out/simulate a new browsing session by dropping all cookies.
+ await page.context().clearCookies();
+ await page.reload();
await page.goto( 'my-account/lost-password/' );
await test.step( 'initiate password reset from my account', async () => {
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-shipping.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-shipping.spec.js
index 16b9f679b44..8649bd7e246 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-shipping.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-calculate-shipping.spec.js
@@ -142,7 +142,10 @@ test.describe(
// Set shipping country to Netherlands
await page.getByLabel( 'Add an address for shipping' ).click();
- await page.getByRole( 'combobox' ).first().fill( 'Netherlands' );
+ await page
+ .getByRole( 'combobox' )
+ .first()
+ .selectOption( 'Netherlands' );
await page.getByLabel( 'Postal code' ).fill( '1011AA' );
await page.getByLabel( 'City' ).fill( 'Amsterdam' );
await page.getByRole( 'button', { name: 'Update' } ).click();
@@ -171,7 +174,10 @@ test.describe(
// Set shipping country to Portugal
await page.getByLabel( 'Add an address for shipping' ).click();
- await page.getByRole( 'combobox' ).first().fill( 'Portugal' );
+ await page
+ .getByRole( 'combobox' )
+ .first()
+ .selectOption( 'Portugal' );
await page.getByLabel( 'Postal code' ).fill( '1000-001' );
await page.getByLabel( 'City' ).fill( 'Lisbon' );
await page.getByRole( 'button', { name: 'Update' } ).click();
@@ -207,7 +213,10 @@ test.describe(
// Set shipping country to Portugal
await page.getByLabel( 'Add an address for shipping' ).click();
- await page.getByRole( 'combobox' ).first().fill( 'Portugal' );
+ await page
+ .getByRole( 'combobox' )
+ .first()
+ .selectOption( 'Portugal' );
await page.getByLabel( 'Postal code' ).fill( '1000-001' );
await page.getByLabel( 'City' ).fill( 'Lisbon' );
await page.getByRole( 'button', { name: 'Update' } ).click();
@@ -238,7 +247,10 @@ test.describe(
// Set shipping country to Portugal
await page.getByLabel( 'Add an address for shipping' ).click();
- await page.getByRole( 'combobox' ).first().fill( 'Portugal' );
+ await page
+ .getByRole( 'combobox' )
+ .first()
+ .selectOption( 'Portugal' );
await page.getByLabel( 'Postal code' ).fill( '1000-001' );
await page.getByLabel( 'City' ).fill( 'Lisbon' );
await page.getByRole( 'button', { name: 'Update' } ).click();
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-coupons.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-coupons.spec.js
index aeca8fbb71d..62d8d6f2cdc 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-coupons.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-block-coupons.spec.js
@@ -137,9 +137,7 @@ test.describe(
await page
.getByRole( 'button', { name: 'Add a coupon' } )
.click();
- await page
- .locator( '#wc-block-components-totals-coupon__input-0' )
- .fill( coupons[ i ].code );
+ await page.getByLabel( 'Enter code' ).fill( coupons[ i ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
@@ -182,9 +180,7 @@ test.describe(
await page
.getByRole( 'button', { name: 'Add a coupon' } )
.click();
- await page
- .locator( '#wc-block-components-totals-coupon__input-0' )
- .fill( coupons[ i ].code );
+ await page.getByLabel( 'Enter code' ).fill( coupons[ i ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
@@ -224,9 +220,7 @@ test.describe(
} ) => {
// try to add two same coupons and verify the error message
await page.getByRole( 'button', { name: 'Add a coupon' } ).click();
- await page
- .locator( '#wc-block-components-totals-coupon__input-0' )
- .fill( coupons[ 0 ].code );
+ await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
@@ -236,9 +230,7 @@ test.describe(
)
).toBeVisible();
await page.getByRole( 'button', { name: 'Add a coupon' } ).click();
- await page
- .locator( '#wc-block-components-totals-coupon__input-0' )
- .fill( coupons[ 0 ].code );
+ await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
@@ -254,9 +246,7 @@ test.describe(
} ) => {
// add coupon with usage limit
await page.getByRole( 'button', { name: 'Add a coupon' } ).click();
- await page
- .locator( '#wc-block-components-totals-coupon__input-0' )
- .fill( couponLimitedCode );
+ await page.getByLabel( 'Enter code' ).fill( couponLimitedCode );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-calculate-tax.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-calculate-tax.spec.js
index 40838b59527..4888e1bb8a9 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-calculate-tax.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/cart-checkout-calculate-tax.spec.js
@@ -963,8 +963,6 @@ test.describe.serial(
} )
).toBeVisible();
- await page.pause();
-
await expect(
page.getByRole( 'cell', {
name: 'Flat rate: $23.00 (incl. tax',
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block-coupons.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block-coupons.spec.js
index 95bdb3f28f6..6db1b13ff81 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block-coupons.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block-coupons.spec.js
@@ -138,9 +138,7 @@ test.describe(
await page
.getByRole( 'button', { name: 'Add a coupon' } )
.click();
- await page
- .locator( '#wc-block-components-totals-coupon__input-0' )
- .fill( coupons[ i ].code );
+ await page.getByLabel( 'Enter code' ).fill( coupons[ i ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
@@ -183,9 +181,7 @@ test.describe(
await page
.getByRole( 'button', { name: 'Add a coupon' } )
.click();
- await page
- .locator( '#wc-block-components-totals-coupon__input-0' )
- .fill( coupons[ i ].code );
+ await page.getByLabel( 'Enter code' ).fill( coupons[ i ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
@@ -225,9 +221,7 @@ test.describe(
} ) => {
// try to add two same coupons and verify the error message
await page.getByRole( 'button', { name: 'Add a coupon' } ).click();
- await page
- .locator( '#wc-block-components-totals-coupon__input-0' )
- .fill( coupons[ 0 ].code );
+ await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
@@ -237,9 +231,7 @@ test.describe(
)
).toBeVisible();
await page.getByRole( 'button', { name: 'Add a coupon' } ).click();
- await page
- .locator( '#wc-block-components-totals-coupon__input-0' )
- .fill( coupons[ 0 ].code );
+ await page.getByLabel( 'Enter code' ).fill( coupons[ 0 ].code );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
@@ -255,9 +247,7 @@ test.describe(
} ) => {
// add coupon with usage limit
await page.getByRole( 'button', { name: 'Add a coupon' } ).click();
- await page
- .locator( '#wc-block-components-totals-coupon__input-0' )
- .fill( couponLimitedCode );
+ await page.getByLabel( 'Enter code' ).fill( couponLimitedCode );
await page.getByText( 'Apply', { exact: true } ).click();
await expect(
page
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block.spec.js
index a993b79ac9b..24ce0cb50f6 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-block.spec.js
@@ -18,7 +18,13 @@ const {
const { getOrderIdFromUrl } = require( '../../utils/order' );
const guestEmail = 'checkout-guest@example.com';
-const newAccountEmail = 'marge-test-account@example.com';
+const newAccountEmail = `marge-${ new Date()
+ .getTime()
+ .toString() }@woocommercecoree2etestsuite.com`;
+const newAccountEmailWithCustomPassword = `homer-${ new Date()
+ .getTime()
+ .toString() }@woocommercecoree2etestsuite.com`;
+const newAccountCustomPassword = 'supersecurepassword123';
const simpleProductName = 'Very Simple Product';
const simpleProductDesc = 'Lorem ipsum dolor.';
@@ -134,22 +140,6 @@ test.describe(
await api.put( 'payment_gateways/cod', {
enabled: true,
} );
- // make sure there's no pre-existing customer that has the same email we're going to use for account creation
- const { data: customersList } = await api.get( 'customers', {
- email: newAccountEmail,
- } );
-
- if ( customersList && customersList.length ) {
- const customerId = customersList[ 0 ].id;
-
- console.log(
- `Customer with email ${ newAccountEmail } exists! Deleting it before starting test...`
- );
-
- await api.delete( `customers/${ customerId }`, {
- force: true,
- } );
- }
// make sure our customer user has a pre-defined billing/shipping address
await api.put( `customers/2`, {
shipping: {
@@ -208,6 +198,12 @@ test.describe(
value: 'no',
}
);
+ await api.put(
+ 'settings/account/woocommerce_registration_generate_password',
+ {
+ value: 'yes',
+ }
+ );
// delete the orders we created
if ( guestOrderId1 ) {
await api.delete( `orders/${ guestOrderId1 }`, {
@@ -233,7 +229,10 @@ test.describe(
await api.get( 'customers' ).then( async ( response ) => {
for ( let i = 0; i < response.data.length; i++ ) {
if (
- response.data[ i ].billing.email === newAccountEmail
+ [
+ newAccountEmail,
+ newAccountEmailWithCustomPassword,
+ ].includes( response.data[ i ].billing.email )
) {
await api.delete(
`customers/${ response.data[ i ].id }`,
@@ -376,12 +375,8 @@ test.describe(
await expect(
page.getByText( '+ Add apartment, suite, etc.' )
).toBeEnabled();
- await expect(
- page.getByLabel( 'United States (US), Country/Region' )
- ).toBeEditable();
- await expect(
- page.getByLabel( 'California, State' )
- ).toBeEditable();
+ await expect( page.getByLabel( 'Country/Region' ) ).toBeEnabled();
+ await expect( page.getByLabel( 'State' ) ).toBeEnabled();
await expect( page.getByLabel( 'City' ) ).toBeEditable();
await expect( page.getByLabel( 'ZIP Code' ) ).toBeEnabled();
await expect(
@@ -445,7 +440,7 @@ test.describe(
// verify shipping details
await page
- .getByLabel( 'Edit address', { exact: true } )
+ .getByLabel( 'Edit shipping address', { exact: true } )
.first()
.click();
await expect(
@@ -476,7 +471,7 @@ test.describe(
// verify billing details
await page
- .getByLabel( 'Edit address', { exact: true } )
+ .getByLabel( 'Edit billing address', { exact: true } )
.last()
.click();
await expect(
@@ -774,7 +769,7 @@ test.describe(
.waitFor( { state: 'visible' } );
// click to log in and make sure you are on the same page after logging in
- await page.locator( 'text=Log in.' ).click();
+ await page.locator( 'text=Log in' ).click();
await page
.locator( 'input[name="username"]' )
.fill( customer.username );
@@ -787,7 +782,7 @@ test.describe(
).toBeVisible();
await page
- .getByLabel( 'Edit address', { exact: true } )
+ .getByLabel( 'Edit shipping address', { exact: true } )
.first()
.click();
@@ -870,11 +865,11 @@ test.describe(
// check create account during checkout
await expect(
- page.getByLabel( 'Create an account?' )
+ page.getByLabel( 'Create an account' )
).toBeVisible();
- await page.getByLabel( 'Create an account?' ).check();
+ await page.getByLabel( 'Create an account' ).check();
await expect(
- page.getByLabel( 'Create an account?' )
+ page.getByLabel( 'Create an account' )
).toBeChecked();
// For flakiness, sometimes the email address is not filled
@@ -925,5 +920,108 @@ test.describe(
newAccountEmail
);
} );
+
+ test( 'can create an account during checkout with custom password', async ( {
+ page,
+ testPage,
+ baseURL,
+ } ) => {
+ const api = new wcApi( {
+ url: baseURL,
+ consumerKey: process.env.CONSUMER_KEY,
+ consumerSecret: process.env.CONSUMER_SECRET,
+ version: 'wc/v3',
+ } );
+ // Password generation off
+ await api.put(
+ 'settings/account/woocommerce_registration_generate_password',
+ {
+ value: 'no',
+ }
+ );
+ await addAProductToCart( page, productId );
+ await page.goto( testPage.slug );
+
+ await expect(
+ page.getByRole( 'heading', { name: testPage.title } )
+ ).toBeVisible();
+
+ // wait for product price to show up in the summary
+ await page
+ .locator(
+ '.wc-block-components-order-summary-item__individual-prices'
+ )
+ .waitFor( { state: 'visible' } );
+
+ // check create account during checkout
+ await expect(
+ page.getByLabel( 'Create an account' )
+ ).toBeVisible();
+ await page.getByLabel( 'Create an account' ).check();
+ await expect(
+ page.getByLabel( 'Create an account' )
+ ).toBeChecked();
+
+ // Fill password field.
+ await expect(
+ page.getByLabel( 'Create a password' )
+ ).toBeVisible();
+ await page.getByLabel( 'Create a password' ).click();
+ await page
+ .getByLabel( 'Create a password' )
+ .fill( newAccountCustomPassword );
+
+ // For flakiness, sometimes the email address is not filled
+ await page.getByLabel( 'Email address' ).click();
+ await page
+ .getByLabel( 'Email address' )
+ .fill( newAccountEmailWithCustomPassword );
+ await expect( page.getByLabel( 'Email address' ) ).toHaveValue(
+ newAccountEmailWithCustomPassword
+ );
+
+ // fill shipping address and check cash on delivery method
+ await fillShippingCheckoutBlocks( page, 'Marge' );
+ await page.getByLabel( 'Cash on delivery' ).check();
+ await expect( page.getByLabel( 'Cash on delivery' ) ).toBeChecked();
+
+ // add note to the order
+ await page.getByLabel( 'Add a note to your order' ).check();
+ await page
+ .getByPlaceholder(
+ 'Notes about your order, e.g. special notes for delivery.'
+ )
+ .fill( 'This is to avoid flakiness' );
+
+ // place an order
+ await page.getByRole( 'button', { name: 'Place order' } ).click();
+ await expect(
+ page.getByText( 'Your order has been received' )
+ ).toBeVisible();
+
+ // get order ID from the page
+ newAccountOrderId = getOrderIdFromUrl( page );
+
+ // confirms that an account was created
+ await page.goto( '/my-account/' );
+ await expect(
+ page.getByRole( 'heading', { name: 'My account' } )
+ ).toBeVisible();
+ await page
+ .getByRole( 'navigation' )
+ .getByRole( 'link', { name: 'Log out' } )
+ .click();
+
+ // Log in again.
+ await page.goto( '/my-account/' );
+ await page
+ .locator( '#username' )
+ .fill( newAccountEmailWithCustomPassword );
+ await page.locator( '#password' ).fill( newAccountCustomPassword );
+ await page.locator( 'text=Log in' ).click();
+ await expect(
+ page.getByRole( 'heading', { name: 'My account' } )
+ ).toBeVisible();
+ } );
}
);
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-create-account.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-create-account.spec.js
index f37e44b4003..f0ad43fec8a 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-create-account.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout-create-account.spec.js
@@ -159,7 +159,7 @@ test.describe(
await page.locator( '#billing_phone' ).fill( '123456789' );
await page.locator( '#billing_email' ).fill( billingEmail );
- await page.getByText( 'Create an account?' ).check();
+ await page.getByText( 'Create an account' ).check();
await page.locator( '#place_order' ).click();
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/product-tags-attributes.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/product-tags-attributes.spec.js
index a8db2fa111c..acb5651b7af 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/product-tags-attributes.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/product-tags-attributes.spec.js
@@ -317,7 +317,13 @@ test.describe(
.click();
await expect(
- page.getByRole( 'button', { name: 'Update', exact: true } )
+ // WP 6.6 updates the button text from "Update" to "Save", so we'll need to check for either.
+ page.getByRole( 'button', { name: 'Update', exact: true } ).or(
+ page.getByRole( 'button', {
+ name: 'Save',
+ exact: true,
+ } )
+ )
).toBeVisible();
// go to created page with products showcase
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/shop-products-filter-by-price.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/shop-products-filter-by-price.spec.js
index 851b9dc0daf..61fd3df02ba 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/shop-products-filter-by-price.spec.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/shop-products-filter-by-price.spec.js
@@ -6,6 +6,7 @@ const {
insertBlockByShortcut,
publishPage,
} = require( '../../utils/editor' );
+const { getInstalledWordPressVersion } = require( '../../utils/wordpress' );
const singleProductPrice1 = '10';
const singleProductPrice2 = '50';
@@ -71,7 +72,8 @@ test.describe(
await goToPageEditor( { page } );
await fillPageTitle( page, testPage.title );
await insertBlockByShortcut( page, 'Filter by Price' );
- await insertBlock( page, 'All Products' );
+ const wordPressVersion = await getInstalledWordPressVersion();
+ await insertBlock( page, 'All Products', wordPressVersion );
await publishPage( page, testPage.title );
// go to the page to test filtering products by price
diff --git a/plugins/woocommerce/tests/e2e-pw/utils/editor.js b/plugins/woocommerce/tests/e2e-pw/utils/editor.js
index 2523983fa53..18c73af4714 100644
--- a/plugins/woocommerce/tests/e2e-pw/utils/editor.js
+++ b/plugins/woocommerce/tests/e2e-pw/utils/editor.js
@@ -1,14 +1,13 @@
const { expect } = require( '@playwright/test' );
-const closeWelcomeModal = async ( { page } ) => {
- // Close welcome popup if prompted
- try {
- await page
- .getByLabel( 'Close', { exact: true } )
- .click( { timeout: 5000 } );
- } catch ( error ) {
- // Welcome modal wasn't present, skipping action.
- }
+const closeChoosePatternModal = async ( { page } ) => {
+ const closeModal = page.getByRole( 'button', {
+ name: 'Close',
+ exact: true,
+ } );
+ await page.addLocatorHandler( closeModal, async () => {
+ await closeModal.click();
+ } );
};
const disableWelcomeModal = async ( { page } ) => {
@@ -42,24 +41,54 @@ const getCanvas = async ( page ) => {
const goToPageEditor = async ( { page } ) => {
await page.goto( 'wp-admin/post-new.php?post_type=page' );
await disableWelcomeModal( { page } );
+ await page.waitForResponse(
+ ( response ) =>
+ response.url().includes( '//page' ) && response.status() === 200
+ );
};
const goToPostEditor = async ( { page } ) => {
await page.goto( 'wp-admin/post-new.php' );
await disableWelcomeModal( { page } );
+ await page.waitForResponse(
+ ( response ) =>
+ response.url().includes( '//single' ) && response.status() === 200
+ );
};
const fillPageTitle = async ( page, title ) => {
- await ( await getCanvas( page ) )
- .getByRole( 'textbox', { name: 'Add title' } )
- .fill( title );
+ await ( await getCanvas( page ) ).getByLabel( 'Add title' ).click();
+ await ( await getCanvas( page ) ).getByLabel( 'Add title' ).fill( title );
};
-const insertBlock = async ( page, blockName ) => {
- await page.getByLabel( 'Toggle block inserter' ).click();
+const insertBlock = async ( page, blockName, wpVersion = null ) => {
+ await page
+ .getByRole( 'button', {
+ name: 'Toggle block inserter',
+ expanded: false,
+ } )
+ .click();
await page.getByPlaceholder( 'Search', { exact: true } ).fill( blockName );
await page.getByRole( 'option', { name: blockName, exact: true } ).click();
- await page.getByLabel( 'Toggle block inserter' ).click();
+
+ // In WP 6.6 'Toggle block inserter' button closes the inserter as expected,
+ // but trying to immediately open it again will fail in Playwright, while manually it works.
+ // We have tests that insert multiple blocks and fail because of this.
+ // Using the new 'Close block inserter' button added in WP 6.6 works fine.
+ if ( wpVersion && wpVersion <= 6.5 ) {
+ await page
+ .getByRole( 'button', {
+ name: 'Toggle block inserter',
+ expanded: true,
+ } )
+ .click();
+ } else {
+ await page
+ .getByRole( 'button', {
+ name: 'Close block inserter',
+ } )
+ .click();
+ }
};
const insertBlockByShortcut = async ( page, blockName ) => {
@@ -111,7 +140,7 @@ const publishPage = async ( page, pageTitle ) => {
};
module.exports = {
- closeWelcomeModal,
+ closeChoosePatternModal,
goToPageEditor,
goToPostEditor,
disableWelcomeModal,
diff --git a/plugins/woocommerce/tests/e2e-pw/utils/themes.js b/plugins/woocommerce/tests/e2e-pw/utils/themes.js
index 9f66ccb0edc..0f66505a654 100644
--- a/plugins/woocommerce/tests/e2e-pw/utils/themes.js
+++ b/plugins/woocommerce/tests/e2e-pw/utils/themes.js
@@ -4,9 +4,9 @@ export const DEFAULT_THEME = 'twentytwentythree';
export const activateTheme = ( themeName ) => {
return new Promise( ( resolve, reject ) => {
- const command = `wp-env run tests-cli wp theme activate ${ themeName }`;
+ const command = `wp-env run tests-cli wp theme install ${ themeName } --activate`;
- exec( command, ( error, stdout, stderr ) => {
+ exec( command, ( error, stdout ) => {
if ( error ) {
console.error( `Error executing command: ${ error }` );
return reject( error );
diff --git a/plugins/woocommerce/tests/e2e-pw/utils/wordpress.js b/plugins/woocommerce/tests/e2e-pw/utils/wordpress.js
index 9bc27a5078b..c36a21cd258 100644
--- a/plugins/woocommerce/tests/e2e-pw/utils/wordpress.js
+++ b/plugins/woocommerce/tests/e2e-pw/utils/wordpress.js
@@ -1,3 +1,6 @@
+const { promisify } = require( 'util' );
+const execAsync = promisify( require( 'child_process' ).exec );
+
const getVersionWPLatestMinusOne = async ( { core, github } ) => {
const URL_WP_STABLE_VERSION_CHECK =
'https://api.wordpress.org/core/stable-check/1.0/';
@@ -21,4 +24,18 @@ const getVersionWPLatestMinusOne = async ( { core, github } ) => {
core.setOutput( 'version', latestMinus1 );
};
-module.exports = { getVersionWPLatestMinusOne };
+const getInstalledWordPressVersion = async () => {
+ try {
+ const { stdout } = await execAsync(
+ `pnpm exec wp-env run tests-cli -- wp core version`
+ );
+
+ return Number.parseFloat( stdout.trim() );
+ } catch ( error ) {
+ throw new Error(
+ `Error getting WordPress version: ${ error.message }`
+ );
+ }
+};
+
+module.exports = { getVersionWPLatestMinusOne, getInstalledWordPressVersion };
diff --git a/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php b/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
index d027f9a1efb..85d653f2114 100644
--- a/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
+++ b/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
@@ -43,7 +43,7 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
CodeHacker::disable();
self::$code_hacker_temporary_disables_requested = 1;
} elseif ( self::$code_hacker_temporary_disables_requested > 0 ) {
- self::$code_hacker_temporary_disables_requested++;
+ ++self::$code_hacker_temporary_disables_requested;
}
}
@@ -53,7 +53,7 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
*/
protected static function reenable_code_hacker() {
if ( self::$code_hacker_temporary_disables_requested > 0 ) {
- self::$code_hacker_temporary_disables_requested--;
+ --self::$code_hacker_temporary_disables_requested;
if ( 0 === self::$code_hacker_temporary_disables_requested ) {
CodeHacker::enable();
}
@@ -390,6 +390,13 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
return $matches;
}
+ /**
+ * Clear recorded tracks event.
+ */
+ public function clear_tracks_events() {
+ $events = WC_Tracks_Footer_Pixel::clear_events();
+ }
+
/**
* Assert that a valid tracks event has been recorded.
*
diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php
index 0b24fee4061..bf82ee2d7f9 100644
--- a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php
+++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-product.php
@@ -14,6 +14,7 @@ class WC_Helper_Product {
/**
* Counter to insert unique SKU for concurrent tests.
+ * The starting value ensures no conflicts between existing generators.
*
* @var int $sku_counter
*/
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php
index 0882a72a01e..afde3ff97b8 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php
@@ -24,10 +24,11 @@ class ProductHelper {
/**
* Counter to insert unique SKU for concurrent tests.
+ * The starting value ensures no conflicts between existing generators.
*
* @var int $sku_counter
*/
- private static $sku_counter = 0;
+ private static $sku_counter = 100000;
/**
* Delete a product.
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/templates/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/templates/functions.php
index 6756dd85078..6c9e16b60b3 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/templates/functions.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/templates/functions.php
@@ -193,4 +193,86 @@ class WC_Tests_Template_Functions extends WC_Unit_Test_Case {
$this->assertEquals( $expected_html, $actual_html );
}
+
+ /**
+ * Test: test_radio_not_required_field.
+ */
+ public function test_radio_not_required_field() {
+ $actual_html = woocommerce_form_field(
+ 'test',
+ array(
+ 'type' => 'radio',
+ 'id' => 'test',
+ 'required' => false,
+ 'options' => array(
+ '1' => 'Option 1',
+ '2' => 'Option 2',
+ ),
+ 'return' => true,
+ ),
+ '1'
+ );
+
+ $this->assertStringNotContainsString( 'aria-required', $actual_html );
+ }
+
+ /**
+ * Test: test_radio_required_field.
+ */
+ public function test_radio_required_field() {
+ $actual_html = woocommerce_form_field(
+ 'test',
+ array(
+ 'type' => 'radio',
+ 'id' => 'test_radio',
+ 'required' => true,
+ 'options' => array(
+ '1' => 'Option 1',
+ '2' => 'Option 2',
+ ),
+ 'return' => true,
+ ),
+ '1'
+ );
+ $expected_html = 'Option 1 Option 2
';
+
+ $this->assertEquals( $expected_html, $actual_html );
+ }
+
+ /**
+ * Test: test_checkbox_not_required_field.
+ */
+ public function test_checkbox_not_required_field() {
+ $actual_html = woocommerce_form_field(
+ 'test',
+ array(
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'label' => 'Checkbox',
+ 'return' => true,
+ ),
+ '1'
+ );
+
+ $this->assertStringNotContainsString( 'aria-required', $actual_html );
+ }
+
+ /**
+ * Test: test_checkbox_required_field.
+ */
+ public function test_checkbox_required_field() {
+ $actual_html = woocommerce_form_field(
+ 'test',
+ array(
+ 'type' => 'checkbox',
+ 'required' => true,
+ 'label' => 'Checkbox',
+ 'return' => true,
+ ),
+ '1'
+ );
+ $expected_html = ' Checkbox *
';
+
+ $this->assertEquals( $expected_html, $actual_html );
+ }
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders.php
index 89c84f26ae6..38aeb49eed6 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders.php
@@ -105,6 +105,8 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case {
$order = WC_Helper_Order::create_order( 1, $product );
$order->set_status( 'completed' );
$order->set_total( 100 ); // $25 x 4.
+ // Make sure the order is paid at least a minute ago to avoid issues with the same timestamp - undeterministic order.
+ $order->set_date_paid( $order->get_date_paid()->modify( '-1 minute' ) );
$order->save();
$refund = wc_create_refund(
@@ -132,8 +134,8 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 2, count( $reports ) );
- $order_report = $reports[0];
- $refund_report = $reports[1];
+ $refund_report = $reports[0];
+ $order_report = $reports[1];
$this->assertEquals( $order->get_id(), $order_report['order_id'] );
$this->assertEquals( $refund->get_id(), $refund_report['order_id'] );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-taxes.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-taxes.php
index 32cdd67c2c6..3f896cc5b78 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-taxes.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-taxes.php
@@ -141,98 +141,14 @@ class WC_Admin_Tests_API_Reports_Taxes extends WC_REST_Unit_Test_Case {
* @since 3.5.0
*/
public function test_get_reports_taxes_param() {
- global $wpdb;
- wp_set_current_user( $this->user );
- WC_Helper_Reports::reset_stats_dbs();
- // Populate all of the data.
- $product = new WC_Product_Simple();
- $product->set_name( 'Test Product' );
- $product->set_regular_price( 25 );
- $product->save();
+ $this->create_sample_taxes();
- $wpdb->insert(
- $wpdb->prefix . 'woocommerce_tax_rates',
- array(
- 'tax_rate_id' => 1,
- 'tax_rate' => '7',
- 'tax_rate_country' => 'US',
- 'tax_rate_state' => 'NY',
- 'tax_rate_name' => 'TestTax',
- 'tax_rate_priority' => 1,
- 'tax_rate_order' => 1,
- )
- );
-
- $wpdb->insert(
- $wpdb->prefix . 'woocommerce_tax_rates',
- array(
- 'tax_rate_id' => 2,
- 'tax_rate' => '8',
- 'tax_rate_country' => 'CA',
- 'tax_rate_state' => 'ON',
- 'tax_rate_name' => 'TestTax 2',
- 'tax_rate_priority' => 1,
- 'tax_rate_order' => 1,
- )
- );
-
- $tax_item = new WC_Order_Item_Tax();
- $tax_item->set_rate( 1 );
- $tax_item->set_tax_total( 5 );
- $tax_item->set_shipping_tax_total( 2 );
-
- $order = WC_Helper_Order::create_order( 1, $product );
- $order->add_item( $tax_item );
- $order->set_status( 'completed' );
- $order->set_total( 100 ); // $25 x 4.
- $order->save();
-
- $tax_item_ca = new WC_Order_Item_Tax();
- $tax_item_ca->set_rate( 2 );
- $tax_item_ca->set_tax_total( 15 );
- $tax_item_ca->set_shipping_tax_total( 0 );
-
- $order_ca = WC_Helper_Order::create_order( 1, $product );
- $order_ca->set_shipping_state( 'ON' );
- $order_ca->set_shipping_country( 'CA' );
- $order_ca->add_item( $tax_item_ca );
- $order_ca->set_status( 'completed' );
- $order_ca->set_total( 100 ); // $25 x 4.
- $order_ca->save();
- $order_ca->calculate_totals( true );
-
- // @todo Remove this once order data is synced to wc_order_tax_lookup
- $wpdb->insert(
- $wpdb->prefix . 'wc_order_tax_lookup',
- array(
- 'order_id' => $order->get_id(),
- 'tax_rate_id' => 1,
- 'date_created' => gmdate( 'Y-m-d H:i:s' ),
- 'shipping_tax' => 2,
- 'order_tax' => 5,
- 'total_tax' => 7,
- )
- );
- $wpdb->insert(
- $wpdb->prefix . 'wc_order_tax_lookup',
- array(
- 'order_id' => $order_ca->get_id(),
- 'tax_rate_id' => 2,
- 'date_created' => gmdate( 'Y-m-d H:i:s' ),
- 'shipping_tax' => 2,
- 'order_tax' => 5,
- 'total_tax' => 7,
- )
- );
-
- WC_Helper_Queue::run_all_pending();
-
- $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
- $request = new WP_REST_Request( 'GET', $this->endpoint );
+ $request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params(
array(
- 'taxes' => '1,2',
+ 'taxes' => '1,2,3',
+ 'per_page' => '2',
)
);
$response = $this->server->dispatch( $request );
@@ -241,29 +157,74 @@ class WC_Admin_Tests_API_Reports_Taxes extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 2, count( $reports ) );
- $tax_report = reset( $reports );
+ // Results are ordered by tax_rate_id desc by default.
+ $expected = array(
+ 3 => array(
+ 'tax_rate_id' => 3,
+ 'tax_rate' => 9.0,
+ 'country' => 'ES',
+ 'state' => 'B',
+ 'total_tax' => 9.9 * 2,
+ 'order_tax' => 9.0 * 2,
+ 'shipping_tax' => 0.9 * 2,
+ 'orders_count' => 2,
+ ),
+ 2 => array(
+ 'tax_rate_id' => 2,
+ 'tax_rate' => 8.0,
+ 'country' => 'CA',
+ 'state' => 'ON',
+ 'total_tax' => 8.8,
+ 'order_tax' => 8,
+ 'shipping_tax' => 0.8,
+ 'orders_count' => 1,
+ ),
- $this->assertEquals( 2, $tax_report['tax_rate_id'] );
- $this->assertEquals( 8.0, $tax_report['tax_rate'] );
- $this->assertEquals( 'CA', $tax_report['country'] );
- $this->assertEquals( 'ON', $tax_report['state'] );
- $this->assertEquals( 8.8, $tax_report['total_tax'] );
- $this->assertEquals( 8, $tax_report['order_tax'] );
- $this->assertEquals( 0.8, $tax_report['shipping_tax'] );
- $this->assertEquals( 1, $tax_report['orders_count'] );
+ );
- $tax_report = next( $reports );
+ foreach ( $reports as $tax_report ) {
+ $expected_report = $expected[ $tax_report['tax_rate_id'] ];
+ foreach ( array_keys( $expected_report ) as $key ) {
+ $this->assertEquals( $expected_report[ $key ], $tax_report[ $key ] );
+ }
+ }
- $this->assertEquals( 1, $tax_report['tax_rate_id'] );
- $this->assertEquals( 7, $tax_report['tax_rate'] );
- $this->assertEquals( 'US', $tax_report['country'] );
- $this->assertEquals( 'NY', $tax_report['state'] );
- $this->assertEquals( 7, $tax_report['total_tax'] );
- $this->assertEquals( 5, $tax_report['order_tax'] );
- $this->assertEquals( 2, $tax_report['shipping_tax'] );
- $this->assertEquals( 1, $tax_report['orders_count'] );
+ // Get next page.
+ $request->set_query_params(
+ array(
+ 'taxes' => '1,2,3',
+ 'per_page' => '2',
+ 'page' => '2',
+ )
+ );
+ $response = $this->server->dispatch( $request );
+ $reports = $response->get_data();
+
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertEquals( 1, count( $reports ) );
+
+ $expected = array(
+ 1 => array(
+ 'tax_rate_id' => 1,
+ 'tax_rate' => 7,
+ 'country' => 'US',
+ 'state' => 'NY',
+ 'total_tax' => 7,
+ 'order_tax' => 5,
+ 'shipping_tax' => 2,
+ 'orders_count' => 1,
+ ),
+ );
+
+ foreach ( $reports as $tax_report ) {
+ $expected_report = $expected[ $tax_report['tax_rate_id'] ];
+ foreach ( array_keys( $expected_report ) as $key ) {
+ $this->assertEquals( $expected_report[ $key ], $tax_report[ $key ] );
+ }
+ }
}
+
/**
* Test getting reports without valid permissions.
*
@@ -300,4 +261,161 @@ class WC_Admin_Tests_API_Reports_Taxes extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'shipping_tax', $properties );
$this->assertArrayHasKey( 'orders_count', $properties );
}
+
+ /**
+ * Create sample taxes.
+ */
+ protected function create_sample_taxes() {
+ global $wpdb;
+ wp_set_current_user( $this->user );
+ WC_Helper_Reports::reset_stats_dbs();
+
+ // Populate all of the data.
+ $product = new WC_Product_Simple();
+ $product->set_name( 'Test Product' );
+ $product->set_regular_price( 25 );
+ $product->save();
+
+ $wpdb->insert(
+ $wpdb->prefix . 'woocommerce_tax_rates',
+ array(
+ 'tax_rate_id' => 1,
+ 'tax_rate' => '7',
+ 'tax_rate_country' => 'US',
+ 'tax_rate_state' => 'NY',
+ 'tax_rate_name' => 'TestTax',
+ 'tax_rate_priority' => 1,
+ 'tax_rate_order' => 1,
+ )
+ );
+
+ $wpdb->insert(
+ $wpdb->prefix . 'woocommerce_tax_rates',
+ array(
+ 'tax_rate_id' => 2,
+ 'tax_rate' => '8',
+ 'tax_rate_country' => 'CA',
+ 'tax_rate_state' => 'ON',
+ 'tax_rate_name' => 'TestTax 2',
+ 'tax_rate_priority' => 1,
+ 'tax_rate_order' => 1,
+ )
+ );
+
+ $wpdb->insert(
+ $wpdb->prefix . 'woocommerce_tax_rates',
+ array(
+ 'tax_rate_id' => 3,
+ 'tax_rate' => '9',
+ 'tax_rate_country' => 'ES',
+ 'tax_rate_state' => 'B',
+ 'tax_rate_name' => 'TestTax 3',
+ 'tax_rate_priority' => 1,
+ 'tax_rate_order' => 1,
+ )
+ );
+
+ $tax_item = new WC_Order_Item_Tax();
+ $tax_item->set_rate( 1 );
+ $tax_item->set_tax_total( 5 );
+ $tax_item->set_shipping_tax_total( 2 );
+
+ $order = WC_Helper_Order::create_order( 1, $product );
+ $order->add_item( $tax_item );
+ $order->set_status( 'completed' );
+ $order->set_total( 100 ); // $25 x 4.
+ $order->save();
+
+ $tax_item_ca = new WC_Order_Item_Tax();
+ $tax_item_ca->set_rate( 2 );
+ $tax_item_ca->set_tax_total( 15 );
+ $tax_item_ca->set_shipping_tax_total( 0 );
+
+ $order_ca = WC_Helper_Order::create_order( 1, $product );
+ $order_ca->set_shipping_state( 'ON' );
+ $order_ca->set_shipping_country( 'CA' );
+ $order_ca->add_item( $tax_item_ca );
+ $order_ca->set_status( 'completed' );
+ $order_ca->set_total( 100 ); // $25 x 4.
+ $order_ca->save();
+ $order_ca->calculate_totals( true );
+
+ $tax_item_es = new WC_Order_Item_Tax();
+ $tax_item_es->set_rate( 3 );
+ $tax_item_es->set_tax_total( 15 );
+ $tax_item_es->set_shipping_tax_total( 0 );
+
+ $order_es = WC_Helper_Order::create_order( 1, $product );
+ $order_es->set_shipping_state( 'B' );
+ $order_es->set_shipping_country( 'ES' );
+ $order_es->add_item( $tax_item_es );
+ $order_es->set_status( 'completed' );
+ $order_es->set_total( 100 ); // $25 x 4.
+ $order_es->save();
+ $order_es->calculate_totals( true );
+
+ $tax_item_es_2 = new WC_Order_Item_Tax();
+ $tax_item_es_2->set_rate( 3 );
+ $tax_item_es_2->set_tax_total( 15 );
+ $tax_item_es_2->set_shipping_tax_total( 0 );
+
+ $order_es_2 = WC_Helper_Order::create_order( 1, $product );
+ $order_es_2->set_shipping_state( 'B' );
+ $order_es_2->set_shipping_country( 'ES' );
+ $order_es_2->add_item( $tax_item_es_2 );
+ $order_es_2->set_status( 'completed' );
+ $order_es_2->set_total( 100 ); // $25 x 4.
+ $order_es_2->save();
+ $order_es_2->calculate_totals( true );
+
+ // @todo Remove this once order data is synced to wc_order_tax_lookup
+ $wpdb->insert(
+ $wpdb->prefix . 'wc_order_tax_lookup',
+ array(
+ 'order_id' => $order->get_id(),
+ 'tax_rate_id' => 1,
+ 'date_created' => gmdate( 'Y-m-d H:i:s' ),
+ 'shipping_tax' => 2,
+ 'order_tax' => 5,
+ 'total_tax' => 7,
+ )
+ );
+ $wpdb->insert(
+ $wpdb->prefix . 'wc_order_tax_lookup',
+ array(
+ 'order_id' => $order_ca->get_id(),
+ 'tax_rate_id' => 2,
+ 'date_created' => gmdate( 'Y-m-d H:i:s' ),
+ 'shipping_tax' => 2,
+ 'order_tax' => 5,
+ 'total_tax' => 7,
+ )
+ );
+
+ $wpdb->insert(
+ $wpdb->prefix . 'wc_order_tax_lookup',
+ array(
+ 'order_id' => $order_es->get_id(),
+ 'tax_rate_id' => 3,
+ 'date_created' => gmdate( 'Y-m-d H:i:s' ),
+ 'shipping_tax' => 2,
+ 'order_tax' => 5,
+ 'total_tax' => 7,
+ )
+ );
+
+ $wpdb->insert(
+ $wpdb->prefix . 'wc_order_tax_lookup',
+ array(
+ 'order_id' => $order_es_2->get_id(),
+ 'tax_rate_id' => 3,
+ 'date_created' => gmdate( 'Y-m-d H:i:s' ),
+ 'shipping_tax' => 2,
+ 'order_tax' => 5,
+ 'total_tax' => 7,
+ )
+ );
+
+ WC_Helper_Queue::run_all_pending();
+ }
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/evaluate-suggestion.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/evaluate-suggestion.php
index 25063d26ed3..15fee01caea 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/evaluate-suggestion.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/evaluate-suggestion.php
@@ -16,12 +16,32 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_EvaluateSuggestion extends WC_Uni
*/
const MOCK_OPTION = 'woocommerce_admin_mock_gateway_option';
+ /**
+ * The mock logger.
+ *
+ * @var WC_Logger_Interface|\PHPUnit\Framework\MockObject\MockObject
+ */
+ private $mock_logger;
+
+ /**
+ * Run setup code for unit tests.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ // Have a mock logger used by the rule evaluator.
+ $this->mock_logger = $this->getMockBuilder( 'WC_Logger_Interface' )->getMock();
+ add_filter( 'woocommerce_logging_class', array( $this, 'override_wc_logger' ) );
+ }
+
/**
* Tear down.
*/
public function tearDown(): void {
- parent::tearDown();
delete_option( self::MOCK_OPTION );
+ remove_filter( 'woocommerce_logging_class', array( $this, 'override_wc_logger' ) );
+
+ parent::tearDown();
}
/**
@@ -71,4 +91,271 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_EvaluateSuggestion extends WC_Uni
$evaluated = EvaluateSuggestion::evaluate( (object) $suggestion );
$this->assertTrue( $evaluated->is_visible );
}
+
+ /**
+ * Test that suggestion evaluation logs debug logs when logging is enabled.
+ */
+ public function test_evaluation_logs() {
+ add_filter( 'woocommerce_admin_remote_specs_evaluator_should_log', '__return_true' );
+
+ $suggestion = array(
+ 'id' => 'mock-gateway',
+ 'is_visible' => array(
+ (object) array(
+ 'type' => 'option',
+ 'option_name' => self::MOCK_OPTION,
+ 'value' => 'a',
+ 'default' => null,
+ 'operation' => '=',
+ ),
+ // Only the top-level rules are logged, not the operands.
+ (object) array(
+ 'type' => 'or',
+ 'operands' => array(
+ (object) array(
+ 'type' => 'fail',
+ ),
+ (object) array(
+ 'type' => 'pass',
+ ),
+ ),
+ ),
+ (object) array(
+ 'type' => 'option',
+ 'option_name' => self::MOCK_OPTION,
+ 'value' => 'b', // This will fail the rule.
+ 'default' => null,
+ 'operation' => '=',
+ ),
+ ),
+ );
+ update_option( self::MOCK_OPTION, 'a' );
+
+ $this->mock_logger_debug_calls(
+ array(
+ array(
+ '[mock-gateway] option: passed',
+ array( 'source' => 'unit-tests' ),
+ ),
+ array(
+ '[mock-gateway] or: passed',
+ array( 'source' => 'unit-tests' ),
+ ),
+ array(
+ '[mock-gateway] option: failed',
+ array( 'source' => 'unit-tests' ),
+ ),
+ )
+ );
+
+ EvaluateSuggestion::evaluate( (object) $suggestion, array( 'source' => 'unit-tests' ) );
+
+ remove_filter( 'woocommerce_admin_remote_specs_evaluator_should_log', '__return_true' );
+ }
+
+ /**
+ * Test that suggestion evaluation doesn't log debug logs when logging is disabled.
+ */
+ public function test_evaluation_doesnt_log() {
+ add_filter( 'woocommerce_admin_remote_specs_evaluator_should_log', '__return_false' );
+
+ $suggestion = array(
+ 'id' => 'mock-gateway',
+ 'is_visible' => array(
+ (object) array(
+ 'type' => 'option',
+ 'option_name' => self::MOCK_OPTION,
+ 'value' => 'a',
+ 'default' => null,
+ 'operation' => '=',
+ ),
+ (object) array(
+ 'type' => 'option',
+ 'option_name' => self::MOCK_OPTION,
+ 'value' => 'b', // This will fail the rule.
+ 'default' => null,
+ 'operation' => '=',
+ ),
+ ),
+ );
+ update_option( self::MOCK_OPTION, 'a' );
+
+ // No debug logs.
+ $this->mock_logger_debug_calls();
+
+ EvaluateSuggestion::evaluate( (object) $suggestion, array( 'source' => 'unit-tests' ) );
+
+ remove_filter( 'woocommerce_admin_remote_specs_evaluator_should_log', '__return_false' );
+ }
+
+ /**
+ * Test that suggestion evaluation logs when rule is not an object.
+ */
+ public function test_evaluation_logs_when_rule_not_object() {
+ add_filter( 'woocommerce_admin_remote_specs_evaluator_should_log', '__return_true' );
+
+ $suggestion = array(
+ 'id' => 'mock-gateway',
+ 'is_visible' => array(
+ (object) array(
+ 'type' => 'option',
+ 'option_name' => self::MOCK_OPTION,
+ 'value' => 'a',
+ 'default' => null,
+ 'operation' => '=',
+ ),
+ array(
+ 'type' => 'option',
+ 'option_name' => self::MOCK_OPTION,
+ 'value' => 'a',
+ 'default' => null,
+ 'operation' => '=',
+ ),
+ ),
+ );
+ update_option( self::MOCK_OPTION, 'a' );
+
+ $this->mock_logger_debug_calls(
+ array(
+ array(
+ '[mock-gateway] option: passed',
+ array( 'source' => 'unit-tests' ),
+ ),
+ array(
+ '[mock-gateway] rule not an object: failed',
+ array( 'source' => 'unit-tests' ),
+ ),
+ )
+ );
+
+ EvaluateSuggestion::evaluate( (object) $suggestion, array( 'source' => 'unit-tests' ) );
+
+ remove_filter( 'woocommerce_admin_remote_specs_evaluator_should_log', '__return_true' );
+ }
+
+ /**
+ * Test that suggestion evaluation logs an anonymous spec.
+ */
+ public function test_evaluation_logs_anonymous_spec() {
+ add_filter( 'woocommerce_admin_remote_specs_evaluator_should_log', '__return_true' );
+
+ $suggestion = array(
+ 'id' => '', // empty ID and no 'title' field.
+ 'is_visible' => array(
+ (object) array(
+ 'type' => 'option',
+ 'option_name' => self::MOCK_OPTION,
+ 'value' => 'a',
+ 'default' => null,
+ 'operation' => '=',
+ ),
+ array(
+ 'type' => 'option',
+ 'option_name' => self::MOCK_OPTION,
+ 'value' => 'a',
+ 'default' => null,
+ 'operation' => '=',
+ ),
+ ),
+ );
+ update_option( self::MOCK_OPTION, 'a' );
+
+ $this->mock_logger_debug_calls(
+ array(
+ array(
+ '[anonymous-suggestion] option: passed',
+ array( 'source' => 'unit-tests' ),
+ ),
+ array(
+ '[anonymous-suggestion] rule not an object: failed',
+ array( 'source' => 'unit-tests' ),
+ ),
+ )
+ );
+
+ EvaluateSuggestion::evaluate( (object) $suggestion, array( 'source' => 'unit-tests' ) );
+
+ remove_filter( 'woocommerce_admin_remote_specs_evaluator_should_log', '__return_true' );
+ }
+
+ /**
+ * Test that suggestion evaluation logs to the default source.
+ */
+ public function test_evaluation_logs_to_default_source() {
+ add_filter( 'woocommerce_admin_remote_specs_evaluator_should_log', '__return_true' );
+
+ $suggestion = array(
+ 'id' => 'mock-gateway',
+ 'is_visible' => array(
+ (object) array(
+ 'type' => 'option',
+ 'option_name' => self::MOCK_OPTION,
+ 'value' => 'a',
+ 'default' => null,
+ 'operation' => '=',
+ ),
+ array(
+ 'type' => 'option',
+ 'option_name' => self::MOCK_OPTION,
+ 'value' => 'a',
+ 'default' => null,
+ 'operation' => '=',
+ ),
+ ),
+ );
+ update_option( self::MOCK_OPTION, 'a' );
+
+ $this->mock_logger_debug_calls(
+ array(
+ array(
+ '[mock-gateway] option: passed',
+ array( 'source' => 'wc-payment-gateway-suggestions' ),
+ ),
+ array(
+ '[mock-gateway] rule not an object: failed',
+ array( 'source' => 'wc-payment-gateway-suggestions' ),
+ ),
+ )
+ );
+
+ EvaluateSuggestion::evaluate( (object) $suggestion );
+
+ remove_filter( 'woocommerce_admin_remote_specs_evaluator_should_log', '__return_true' );
+ }
+
+ /**
+ * Overrides the WC logger.
+ *
+ * @return mixed
+ */
+ public function override_wc_logger() {
+ return $this->mock_logger;
+ }
+
+ /**
+ * Set expectations for the logger debug calls with each consecutive call args.
+ *
+ * @param array $calls_args List of expected arguments for each call.
+ *
+ * @return void
+ */
+ private function mock_logger_debug_calls( array $calls_args = array() ) {
+ if ( empty( $calls_args ) ) {
+ $this->mock_logger
+ ->expects( $this->never() )
+ ->method( 'debug' );
+
+ return;
+ }
+
+ $this->mock_logger
+ ->expects( $this->exactly( count( $calls_args ) ) )
+ ->method( 'debug' )
+ ->willReturnCallback(
+ function ( ...$args ) use ( &$calls_args ) {
+ $expected_args = array_shift( $calls_args );
+ $this->assertSame( $expected_args, $args );
+ }
+ );
+ }
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/payment-gateway-suggestions.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/payment-gateway-suggestions.php
index 4e46bb03939..de568aae6dc 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/payment-gateway-suggestions.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/payment-gateway-suggestions/payment-gateway-suggestions.php
@@ -165,6 +165,8 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_Init extends WC_Unit_Test_Case {
* Test that empty suggestions are replaced with defaults.
*/
public function test_empty_suggestions() {
+ // Arrange.
+ // Make sure there are no specs in the transient.
set_transient(
'woocommerce_admin_' . PaymentGatewaySuggestionsDataSourcePoller::ID . '_specs',
array(
@@ -172,14 +174,67 @@ class WC_Admin_Tests_PaymentGatewaySuggestions_Init extends WC_Unit_Test_Case {
)
);
- $suggestions = PaymentGatewaySuggestions::get_suggestions();
- $stored_transients = get_transient( 'woocommerce_admin_' . PaymentGatewaySuggestionsDataSourcePoller::ID . '_specs' );
+ // Replace the external data sources.
+ add_filter(
+ PaymentGatewaySuggestionsDataSourcePoller::FILTER_NAME,
+ function () {
+ return array(
+ 'payment-gateway-suggestions-data-source.json',
+ );
+ }
+ );
+ // Intercept the request to the data source and return a non-empty array to allow us to
+ // skip defaulting to the default payment gateways suggestions too early.
+ add_filter(
+ 'pre_http_request',
+ function ( $pre, $parsed_args, $url ) {
+ $locale = get_locale();
+
+ if ( 'payment-gateway-suggestions-data-source.json?locale=' . $locale === $url ) {
+ return array(
+ 'body' => wp_json_encode(
+ array(
+ array(
+ 'id' => 'mock-gateway1',
+ ),
+ array(
+ 'id' => 'mock-gateway2',
+ ),
+ )
+ ),
+ );
+ }
+
+ return $pre;
+ },
+ 10,
+ 3
+ );
+
+ // Finally return empty specs that should default the suggestions to the default payment gateways suggestions.
+ add_filter(
+ 'woocommerce_admin_payment_gateway_suggestion_specs',
+ function () {
+ return array();
+ }
+ );
+
+ // Act.
+ $suggestions = PaymentGatewaySuggestions::get_suggestions();
+ $stored_specs_in_transient = get_transient( 'woocommerce_admin_' . PaymentGatewaySuggestionsDataSourcePoller::ID . '_specs' );
+
+ // Assert.
$this->assertEquals( 'bacs', $suggestions[0]->id );
- $this->assertEquals( count( $stored_transients['en_US'] ), count( DefaultPaymentGateways::get_all() ) );
+ $this->assertEquals( count( $stored_specs_in_transient['en_US'] ), count( DefaultPaymentGateways::get_all() ) );
$expires = (int) get_transient( '_transient_timeout_woocommerce_admin_' . PaymentGatewaySuggestionsDataSourcePoller::ID . '_specs' );
$this->assertTrue( ( $expires - time() ) <= 3 * HOUR_IN_SECONDS );
+
+ // Clean up.
+ remove_all_filters( PaymentGatewaySuggestionsDataSourcePoller::FILTER_NAME );
+ remove_all_filters( 'pre_http_request' );
+ remove_all_filters( 'woocommerce_admin_payment_gateway_suggestion_specs' );
}
/**
diff --git a/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js b/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js
index 20fee989208..54918a8aed4 100644
--- a/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js
+++ b/plugins/woocommerce/tests/performance/requests/shopper/my-account-orders.js
@@ -92,7 +92,7 @@ export function myAccountOrders() {
my_account_order_id = findBetween(
response.body,
'my-account/view-order/',
- '/">'
+ '/"'
);
} );
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php b/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
index d05a5b99aa1..ef7a26d47c7 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
@@ -71,6 +71,34 @@ class WC_Checkout_Test extends \WC_Unit_Test_Case {
);
}
+ /**
+ * @testdox the customer notes are correctly sanitized.
+ */
+ public function test_order_notes() {
+ $_POST = array(
+ 'ship_to_different_address' => false,
+ 'order_comments' => 'This text should not save inside an anchor. ',
+ 'payment_method' => 'bacs',
+ );
+ $data = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing
+
+ $errors = new WP_Error();
+
+ $this->sut->validate_posted_data( $data, $errors );
+ $result = $this->sut->create_order( $data );
+
+ $content = wc_get_template_html(
+ 'order/order-details.php',
+ array(
+ 'order_id' => $result,
+ 'show_downloads' => false,
+ )
+ );
+ $this->assertStringNotContainsString( '', $content );
+ $this->assertStringNotContainsString( '',
+ 'payment_method' => 'bacs',
+ 'extensions' => array(
+ 'extension_namespace' => array(
+ 'extension_key' => true,
+ ),
+ ),
+ )
+ );
+
+ $response = rest_get_server()->dispatch( $request );
+ $status = $response->get_status();
+ $data = $response->get_data();
+
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
+ $this->assertEquals( $status, 200, print_r( $data, true ) );
+ $this->assertTrue( $data['customer_id'] > 0 );
+
+ $order_id = $data['order_id'];
+ $totals_mock = new TotalsMock();
+ $content = $totals_mock->render_content( wc_get_order( $order_id ), true, [], '' );
+
+ // Check the anchor tag is not present in the output but the text inside it is.
+ $this->assertStringNotContainsString( ' ', $content );
+ $this->assertStringNotContainsString( '