Core Profiler - Add extensions page (#38405)

* Initial design impl. without the full functionality

* Delete unused icons

* Add is_installed and plugins_page_skipped

* Add plugin-card component to render an installable plugin

* Implement plugins page

* Add loaders for plugins

* Add changelog

* Remove unused type

* Add changelog

* Remove unnecessary return statement

* Add obw/core-profiler

* Replace extensions with plugins

* Temp -- use window.location.href for Woo Home redirection

* Minor: code refactor

* Refactor isntallAndActivatedPlugins

* Skip plugins page when there is no available plugin

* Apply mobile styles

* Update plugins/woocommerce-admin/client/core-profiler/components/plugin-card/plugin-card.scss

Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com>

* Update plugins/woocommerce-admin/client/core-profiler/components/plugin-card/plugin-card.scss

Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com>

* Update plugins/woocommerce-admin/client/core-profiler/style.scss

Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com>

* Bold errored plugin name

* Fix checkbox alignment

* Update changelog

* Fix object type for formatToParts function

* Fix lint issues

* Fix CSS lint issues

* Fallback to en-US when locale is not available

* Fix error with siteLocale

---------

Co-authored-by: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
This commit is contained in:
Moon 2023-05-29 07:45:30 -07:00 committed by GitHub
parent 5e3fe64598
commit 6946ef384a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1122 additions and 110 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Add is_installed and plugins_page_skipped type and added async option for installPlugins.

View File

@ -129,6 +129,7 @@ export type ProfileItems = {
selling_venues?: string | null; selling_venues?: string | null;
setup_client?: boolean | null; setup_client?: boolean | null;
skipped?: boolean | null; skipped?: boolean | null;
plugins_page_skipped?: boolean | null;
/** @deprecated This is always null, the theme step has been removed since WC 7.7. */ /** @deprecated This is always null, the theme step has been removed since WC 7.7. */
theme?: string | null; theme?: string | null;
wccom_connected?: boolean | null; wccom_connected?: boolean | null;
@ -181,4 +182,5 @@ export type Extension = {
name: string; name: string;
is_built_by_wc: boolean; is_built_by_wc: boolean;
is_visible: boolean; is_visible: boolean;
is_installed?: boolean;
}; };

View File

@ -215,20 +215,26 @@ function* handlePluginAPIError(
} }
// Action Creator Generators // Action Creator Generators
export function* installPlugins( plugins: Partial< PluginNames >[] ) { export function* installPlugins(
plugins: Partial< PluginNames >[],
async = false
) {
yield setIsRequesting( 'installPlugins', true ); yield setIsRequesting( 'installPlugins', true );
try { try {
const results: InstallPluginsResponse = yield apiFetch( { const results: InstallPluginsResponse = yield apiFetch( {
path: `${ WC_ADMIN_NAMESPACE }/plugins/install`, path: `${ WC_ADMIN_NAMESPACE }/plugins/install`,
method: 'POST', method: 'POST',
data: { plugins: plugins.join( ',' ) }, data: { plugins: plugins.join( ',' ), async },
} ); } );
if ( results.data.installed.length ) { if ( results.data.installed?.length ) {
yield updateInstalledPlugins( results.data.installed ); yield updateInstalledPlugins( results.data.installed );
} }
if ( Object.keys( results.errors.errors ).length ) { if (
results.errors?.errors &&
Object.keys( results.errors.errors ).length
) {
throw results.errors.errors; throw results.errors.errors;
} }

View File

@ -0,0 +1,37 @@
<svg width="222" height="144" viewBox="0 0 222 144" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="222" height="144" fill="white"/>
<path d="M66.7225 101.881H4.39697V139.681H66.7225V101.881Z" fill="#BEA0F2"/>
<path d="M43.1444 108.18H4.39697V111.78H43.1444V108.18Z" fill="#271B3D"/>
<path d="M66.7223 108.18H47.4619V111.78H66.7223V108.18Z" fill="#271B3D"/>
<path d="M66.7224 129.779H51.5386V133.379H66.7224V129.779Z" fill="#271B3D"/>
<path d="M31.6522 118.98H4.39697V122.58H31.6522V118.98Z" fill="#271B3D"/>
<path d="M66.7224 118.98H35.9087V122.58H66.7224V118.98Z" fill="#271B3D"/>
<path d="M47.221 129.779H4.39697V133.379H47.221V129.779Z" fill="#271B3D"/>
<path d="M181.785 139.723H217.765V121.723H181.785V139.723Z" fill="#BEA0F2"/>
<path d="M141.468 139.709H177.449V121.709H141.468V139.709Z" fill="#BEA0F2"/>
<path d="M101.239 139.676H137.219V121.676H101.239V139.676Z" fill="#BEA0F2"/>
<path d="M159.476 117.443H195.457V99.4434H159.476V117.443Z" fill="#BEA0F2"/>
<path d="M181.767 95.1191H217.747V77.1191H181.767V95.1191Z" fill="#BEA0F2"/>
<path d="M199.757 117.428H217.747V99.4277H199.757V117.428Z" fill="#BEA0F2"/>
<path d="M199.757 72.8203H217.747V54.8203H199.757V72.8203Z" fill="#BEA0F2"/>
<path d="M90.5991 91.3711H29.4321V94.9711H90.5991V91.3711Z" fill="#271B3D"/>
<path d="M90.5991 69.8398H29.4321V91.4398H90.5991V69.8398Z" fill="#BEA0F2"/>
<path d="M46.7316 85.9065L42.9968 82.1697C43.5221 81.3741 43.828 80.4237 43.828 79.4013C43.828 76.6221 41.5684 74.3613 38.7907 74.3613C36.013 74.3613 33.7534 76.6221 33.7534 79.4013C33.7534 82.1805 36.013 84.4413 38.7907 84.4413C39.8125 84.4413 40.7624 84.1353 41.5576 83.6097L45.2924 87.3465L46.7352 85.9029L46.7316 85.9065ZM38.7871 82.4037C37.132 82.4037 35.7863 81.0573 35.7863 79.4013C35.7863 77.7453 37.132 76.3989 38.7871 76.3989C40.4422 76.3989 41.7879 77.7453 41.7879 79.4013C41.7879 81.0573 40.4422 82.4037 38.7871 82.4037Z" fill="#271B3D"/>
<path d="M51.7402 73.1016V87.3504" stroke="#271B3D" stroke-width="0.71" stroke-miterlimit="10"/>
<path d="M90.4335 45.6051C101.828 45.6051 111.065 36.3632 111.065 24.9627C111.065 13.5622 101.828 4.32031 90.4335 4.32031C79.0392 4.32031 69.8022 13.5622 69.8022 24.9627C69.8022 36.3632 79.0392 45.6051 90.4335 45.6051Z" fill="#BEA0F2"/>
<path d="M90.8398 13.4219H90.0303V36.5051H90.8398V13.4219Z" fill="white"/>
<path d="M101.969 25.3686V24.5586H78.898V25.3686H101.969Z" fill="white"/>
<path d="M144.07 74.8765H139.367C140.202 65.7145 138.363 48.5605 124.345 48.5605C110.327 48.5605 108.489 65.7145 109.323 74.8765H104.632L96.8022 112.738H151.899L144.07 74.8765ZM124.345 54.4609C132.646 54.4609 132.632 68.2489 130.243 74.8765H118.452C116.063 68.2489 116.045 54.4609 124.349 54.4609H124.345Z" fill="#BEA0F2"/>
<path d="M138.057 4.32031H122.956V30.4959C126.025 30.8775 128.774 33.6207 128.774 36.4935C128.774 39.3663 126.856 42.2787 123.453 42.2787C120.779 42.2787 119.218 41.3355 117.235 39.5895L116.505 40.2447C117.833 42.3651 121.035 44.9895 124.55 44.9895C130.174 44.9895 134.729 41.1159 134.729 35.4891C134.729 32.7027 133.977 29.2539 131.498 26.4351L135.006 14.5983L217.758 39.2583V4.32031H138.054H138.057Z" fill="#271B3D"/>
<path d="M126.026 42.6777L100.551 114.044" stroke="#261B3C" stroke-width="0.99" stroke-miterlimit="10"/>
<path d="M122.676 42.6777L148.15 114.044" stroke="#261B3C" stroke-width="0.99" stroke-miterlimit="10"/>
<path d="M65.4848 4.32031H4.31787V65.5203H65.4848V4.32031Z" fill="#271B3D"/>
<path d="M43.695 38.1169C43.695 26.1361 47.588 8.3125 59.6703 8.3125L47.0951 43.2217H10.8159C10.8159 39.2437 16.0943 29.5273 43.695 38.1133V38.1169Z" fill="#BEA0F2"/>
<path d="M15.1118 62.7235L24.1357 28.4227H38.3372L46.6344 62.7235H48.1779L39.226 25.7695H23.265L13.561 62.7235H15.1118Z" fill="white"/>
<path d="M23.2651 25.7695L24.1359 28.4227H38.3374L39.2261 25.7695H23.2651Z" fill="#BEA0F2"/>
<path d="M153.166 83.2743L165.183 87.4827L175.966 80.7075L177.391 68.0463L168.389 59.0391L155.735 60.4647L148.959 71.2503L153.166 83.2743Z" fill="#BEA0F2"/>
<path d="M172.21 25.6863L180.831 28.2567V18.4287L203.369 4.32031H190.197L172.21 15.5343V25.6863Z" fill="#BEA0F2"/>
<path d="M157.49 21.3015L165.553 23.7027V14.2491L181.633 4.32031H169.555L157.49 11.8443V21.3015Z" fill="#BEA0F2"/>
<path d="M169.58 66.9961L157.652 78.9337" stroke="#271B3D" stroke-width="0.99" stroke-miterlimit="10"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M127.641 94.6549C129.793 93.3409 132.351 91.1197 132.351 87.4549C132.351 82.6057 128.652 79.5781 124.345 79.5781C120.038 79.5781 116.339 82.6057 116.339 87.4549C116.339 91.1233 118.898 93.3445 121.049 94.6549L115.264 108.036H133.43L127.645 94.6549H127.641Z" fill="#271B3D"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,53 @@
<svg width="224" height="144" viewBox="0 0 224 144" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1433_7378)">
<rect width="222.12" height="144" transform="translate(0.94043)" fill="white"/>
<path d="M223.028 139.32H218.708V143.64H223.028V139.32Z" fill="white"/>
<path d="M207.94 15.1191V51.1191H211.54V15.1191H207.94Z" fill="#BEA0F2"/>
<path d="M215.141 4.31914V51.1191H218.741V4.31914H215.141Z" fill="#BEA0F2"/>
<path d="M200.74 25.5591V51.1191H204.34V25.5591H200.74Z" fill="#BEA0F2"/>
<path d="M91.9029 136.25L100.606 112.32H123.035L131.738 136.25H148.541V139.68H111.821H75.1006V136.25H91.9029Z" fill="#BEA0F2"/>
<path d="M95.8758 40.3672H34.6758V101.567H95.8758V40.3672Z" fill="#BEA0F2"/>
<path d="M49.987 71.8594L42.733 74.4802C40.0654 75.445 39.8098 77.1514 40.4974 78.3142C41.329 79.7146 43.3126 79.5814 45.4798 78.1342C47.4994 76.7842 50.5954 74.6242 51.5998 73.9006L49.987 71.8594Z" fill="#271B3D"/>
<path d="M80.562 71.8594C80.562 71.8594 86.61 74.0446 87.816 74.4802C90.4836 75.445 90.7392 77.1514 90.0516 78.3142C89.22 79.7146 87.2364 79.5814 85.0692 78.1342C83.0496 76.7842 79.9536 74.6242 78.9492 73.9006L80.562 71.8594Z" fill="#271B3D"/>
<path d="M48.3779 65.4412L57.2807 77.3176L57.6515 77.1088L55.2683 63.6016L48.3779 65.4412Z" fill="white"/>
<path d="M54.2248 77.0396C50.2756 77.0396 47.626 73.5476 47.626 70.0448C47.626 66.218 51.0748 64.7168 54.2932 64.7168C57.5116 64.7168 61.1584 66.398 61.1584 69.8C61.1584 73.7024 58.2928 77.0396 54.2248 77.0396Z" fill="#BEA0F2"/>
<path d="M56.4964 76.6652C56.7484 76.5788 56.9896 76.478 57.2236 76.3628L55.1752 64.76C54.88 64.7312 54.5848 64.7168 54.2932 64.7168C52.3168 64.7168 50.254 65.2856 48.958 66.6068L56.4964 76.6652Z" fill="white"/>
<path d="M69.3916 69.8C69.3916 66.398 73.0384 64.7168 76.2568 64.7168C79.4752 64.7168 82.924 66.218 82.924 70.0448C82.924 73.5512 80.2708 77.0396 76.3252 77.0396C72.2572 77.0396 69.3916 73.7024 69.3916 69.8Z" fill="#BEA0F2"/>
<path d="M74.0534 76.6652C73.8014 76.5788 73.5602 76.478 73.3262 76.3628L75.3746 64.76C75.6698 64.7312 75.965 64.7168 76.2566 64.7168C78.233 64.7168 80.2958 65.2856 81.5918 66.6068L74.0534 76.6652Z" fill="white"/>
<path d="M76.3934 62.6367C69.2078 62.6367 67.2242 67.8495 65.273 67.8495C63.3218 67.8495 61.3418 62.6367 54.1526 62.6367C49.6022 62.6367 44.8466 65.5635 42.377 66.7515V70.3191H45.0302C45.3974 72.6051 47.2442 79.2039 54.221 79.2039C62.0186 79.2039 62.1914 71.2623 65.2694 71.2623C68.3474 71.2623 68.5238 79.2039 76.3178 79.2039C83.2946 79.2039 85.1414 72.6051 85.5086 70.3191H88.1618V66.7515C85.6922 65.5635 80.9366 62.6367 76.3862 62.6367H76.3934ZM54.2246 77.0403C50.2754 77.0403 47.6258 73.5483 47.6258 70.0455C47.6258 66.2187 51.0746 64.7175 54.293 64.7175C57.5114 64.7175 61.1582 66.3987 61.1582 69.8007C61.1582 73.7031 58.2926 77.0403 54.2246 77.0403ZM76.325 77.0403C72.257 77.0403 69.3914 73.7031 69.3914 69.8007C69.3914 66.3987 73.0382 64.7175 76.2566 64.7175C79.475 64.7175 82.9238 66.2187 82.9238 70.0455C82.9238 73.5519 80.2705 77.0403 76.325 77.0403Z" fill="#271B3D"/>
<path d="M161.461 47.6055H100.261V51.2055H161.461V47.6055Z" fill="#271B3D"/>
<path d="M130.861 54.6484H100.261V58.2484H130.861V54.6484Z" fill="#271B3D"/>
<path d="M161.461 40.4062H100.261V44.0062H161.461V40.4062Z" fill="#271B3D"/>
<path d="M165.011 75.7299V85.6191H181.596C181.596 85.6191 179.814 89.2515 179.814 93.3159C179.814 97.3803 182.438 101.463 187.496 101.463C192.554 101.463 195.179 97.2435 195.179 93.3159C195.179 89.3883 193.397 85.6191 193.397 85.6191H205.522V79.3587V75.7299C211.566 79.1499 218.64 77.9907 218.64 70.8915C218.64 63.7923 211.566 62.6331 205.522 66.0531V56.1639H193.397C193.397 56.1639 195.179 52.5315 195.179 48.4671C195.179 44.4027 192.554 40.3203 187.496 40.3203C182.438 40.3203 179.814 44.5395 179.814 48.4671C179.814 52.3947 181.596 56.1639 181.596 56.1639H165.011V66.0531C171.055 62.6331 178.129 63.7923 178.129 70.8915C178.129 77.9907 171.055 79.1499 165.011 75.7299Z" fill="#271B3D"/>
<path d="M161.461 92.5195H100.261V101.52H161.461V92.5195Z" fill="#BEA0F2"/>
<path d="M35.8607 126.114V121.927L32.3962 121.474C32.0854 120.021 31.5161 118.665 30.7295 117.455L32.8569 114.683L29.898 111.725L27.1264 113.852C25.9167 113.065 24.5608 112.496 23.1076 112.185L22.6544 108.721H18.4671L18.0139 112.185C16.5606 112.496 15.2048 113.065 13.995 113.852L11.2234 111.725L8.26456 114.683L10.392 117.455C9.60542 118.665 9.03612 120.021 8.72525 121.474L5.26074 121.927V126.114L8.72525 126.568C9.03986 128.021 9.61291 129.377 10.392 130.586L8.26456 133.358L11.2234 136.317L13.995 134.189C15.2048 134.976 16.5606 135.545 18.0176 135.856L18.4671 139.321H22.6544L23.1076 135.856C24.5608 135.545 25.9167 134.976 27.1264 134.189L29.898 136.317L32.8569 133.358L30.7295 130.586C31.5161 129.377 32.0854 128.021 32.3962 126.568L35.8607 126.114ZM20.6244 130.68C16.7666 130.68 13.6392 127.553 13.6392 123.695C13.6392 119.837 16.7666 116.71 20.6244 116.71C24.4822 116.71 27.6096 119.837 27.6096 123.695C27.6096 127.553 24.4822 130.68 20.6244 130.68Z" fill="#271B3D"/>
<path d="M189.263 25.5859H34.6758V36.3859H189.263V25.5859Z" fill="#271B3D"/>
<path d="M38.956 32.5586C39.8945 32.5586 40.6552 31.7978 40.6552 30.8594C40.6552 29.9209 39.8945 29.1602 38.956 29.1602C38.0176 29.1602 37.2568 29.9209 37.2568 30.8594C37.2568 31.7978 38.0176 32.5586 38.956 32.5586Z" fill="white"/>
<path d="M45.1152 32.5586C46.0537 32.5586 46.8144 31.7978 46.8144 30.8594C46.8144 29.9209 46.0537 29.1602 45.1152 29.1602C44.1768 29.1602 43.416 29.9209 43.416 30.8594C43.416 31.7978 44.1768 32.5586 45.1152 32.5586Z" fill="white"/>
<path d="M51.2793 32.5586C52.2177 32.5586 52.9785 31.7978 52.9785 30.8594C52.9785 29.9209 52.2177 29.1602 51.2793 29.1602C50.3408 29.1602 49.5801 29.9209 49.5801 30.8594C49.5801 31.7978 50.3408 32.5586 51.2793 32.5586Z" fill="white"/>
<path d="M34.7803 106.301V107.167C34.7803 110.266 37.3687 112.781 40.5655 112.781H183.583C186.776 112.781 189.368 110.269 189.368 107.167V106.301H34.7803Z" fill="#BEA0F2"/>
<path d="M189.368 21.5991V20.7328C189.368 17.6343 186.779 15.1191 183.583 15.1191H40.5655C37.3723 15.1191 34.7803 17.6308 34.7803 20.7328V21.5991H189.368Z" fill="#BEA0F2"/>
<path d="M218.644 130.666C218.644 135.4 214.842 139.249 210.126 139.321H173.665C168.884 139.321 165.011 135.447 165.011 130.666C165.011 125.885 168.884 122.012 173.665 122.012H210.144C214.853 122.012 218.647 125.939 218.647 130.666H218.644Z" fill="#BEA0F2"/>
<path d="M209.946 136.818C213.344 136.818 216.099 134.064 216.099 130.666C216.099 127.268 213.344 124.514 209.946 124.514C206.548 124.514 203.794 127.268 203.794 130.666C203.794 134.064 206.548 136.818 209.946 136.818Z" fill="#271B3D"/>
<path d="M120.662 67.6445H100.261V88.0457H120.662V67.6445Z" fill="#BEA0F2"/>
<path d="M104.93 77.8438H115.992" stroke="#271B3D" stroke-width="0.99" stroke-miterlimit="10"/>
<path d="M110.459 72.3145V83.3773" stroke="#271B3D" stroke-width="0.99" stroke-miterlimit="10"/>
<path d="M8.72394 103.828C6.83034 103.828 5.28954 102.309 5.26074 100.419V7.78351C5.26074 5.87191 6.81234 4.32031 8.72394 4.32031C10.6355 4.32031 12.1871 5.87191 12.1871 7.78351V100.426C12.1871 102.309 10.6175 103.828 8.72394 103.828Z" fill="#BEA0F2"/>
<path d="M12.1871 35.1003V31.5939L5.26074 26.8203V30.3267L12.1871 35.1003Z" fill="#271B3D"/>
<path d="M12.1871 41.907V38.4006L5.26074 33.627V37.1334L12.1871 41.907Z" fill="#271B3D"/>
<path d="M12.1871 48.7119V45.2091L5.26074 40.4355V43.9419L12.1871 48.7119Z" fill="#271B3D"/>
<path d="M12.1871 55.5186V52.0158L5.26074 47.2422V50.7486L12.1871 55.5186Z" fill="#271B3D"/>
<path d="M12.1871 62.3272V58.8244L5.26074 54.0508V57.5572L12.1871 62.3272Z" fill="#271B3D"/>
<path d="M12.1871 69.1338V65.631L5.26074 60.8574V64.3602L12.1871 69.1338Z" fill="#271B3D"/>
<path d="M12.1871 75.9424V72.436L5.26074 67.666V71.1688L12.1871 75.9424Z" fill="#271B3D"/>
<path d="M12.1871 82.7491V79.2427L5.26074 74.4727V77.9755L12.1871 82.7491Z" fill="#271B3D"/>
<path d="M12.1871 89.5577V86.0512L5.26074 81.2812V84.784L12.1871 89.5577Z" fill="#271B3D"/>
<path d="M12.1871 96.3659V92.8595L5.26074 88.0859V91.5923L12.1871 96.3659Z" fill="#271B3D"/>
<path d="M5.26074 94.8926V98.399L11.3735 102.611C11.8811 102.021 12.1871 101.261 12.1871 100.426V99.6662L5.26074 94.8926Z" fill="#271B3D"/>
</g>
<defs>
<clipPath id="clip0_1433_7378">
<rect width="222.12" height="144" fill="white" transform="translate(0.94043)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -0,0 +1,122 @@
.woocommerce-profiler-plugins-plugin-card {
max-width: 492px;
display: flex;
flex-direction: row;
box-sizing: border-box;
border: 1px solid #e0e0e0;
border-radius: 2px;
padding: 26px;
align-items: flex-start;
flex: 0 0 48%;
@include breakpoint( '<782px' ) {
width: 100%;
padding: 16px;
flex: 100%;
max-width: 100%;
}
input {
margin: 3px 26px 0 0;
width: 20px;
height: 20px;
border: 1px solid $gray-700;
border-radius: 2px;
background: #fff;
}
img {
margin-right: 12px;
width: 25px;
height: 25px;
@include breakpoint( '<782px' ) {
align-self: center;
}
}
h3 {
font-size: 14px;
line-height: 20px;
margin: 0 0 8px 0;
padding: 0;
@include breakpoint( '<782px' ) {
margin: 0;
}
}
p {
font-size: 13px;
line-height: 16px;
color: $gray-700;
margin: 0;
padding: 0;
a {
color: inherit;
}
@include breakpoint( '<782px' ) {
display: none;
}
}
.components-checkbox-control {
.components-checkbox-control__input,
components-checkbox-control__input-container {
width: 16px;
height: 16px;
&:focus {
box-shadow: none;
}
}
.components-checkbox-control__input[type='checkbox']:checked {
box-shadow: 0 0 0 1px #fff,
0 0 0 2px
var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba));
outline: 1px solid transparent;
}
.components-checkbox-control__checked {
left: -1px;
top: -1px;
width: 18px;
height: 18px;
}
}
.components-checkbox-control {
@include breakpoint( '<782px' ) {
align-self: center;
}
.components-base-control__field {
@include breakpoint( '<782px' ) {
margin-bottom: 0 !important;
}
.components-checkbox-control__input-container {
@include breakpoint( '<782px' ) {
height: inherit;
}
}
}
}
.woocommerce-profiler-plugins-plugin-card-text {
@include breakpoint( '<782px' ) {
align-self: center;
}
}
.woocommerce-profiler-plugins-plugin-card-text-header {
display: flex;
gap: 8px;
&.installed {
span {
padding: 0 10px;
height: 20px;
background: #b8e6bf;
border-radius: 2px;
color: #00450c;
font-size: 11px;
font-weight: 500;
@include breakpoint( '<782px' ) {
display: none;
}
}
}
}
}

View File

@ -0,0 +1,59 @@
/**
* External dependencies
*/
import { ReactNode } from 'react';
import { CheckboxControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import classnames from 'classnames';
/**
* Internal dependencies
*/
import sanitizeHTML from '~/lib/sanitize-html';
import './plugin-card.scss';
export const PluginCard = ( {
installed = false,
icon,
title,
onChange,
checked = false,
description,
}: {
// Checkbox will be hidden if true
installed?: boolean;
key?: string;
icon: ReactNode;
title: string | ReactNode;
description: string | ReactNode;
checked?: boolean;
onChange?: () => void;
} ) => {
return (
<div className="woocommerce-profiler-plugins-plugin-card">
{ ! installed && (
<CheckboxControl
checked={ checked }
onChange={ onChange ? onChange : () => {} }
/>
) }
{ icon }
<div className="woocommerce-profiler-plugins-plugin-card-text">
<div
className={ classnames(
'woocommerce-profiler-plugins-plugin-card-text-header',
{
installed,
}
) }
>
<h3>{ title }</h3>
{ installed && (
<span>{ __( 'Installed', 'woocommerce' ) }</span>
) }
</div>
<p dangerouslySetInnerHTML={ sanitizeHTML( description ) } />
</div>
</div>
);
};

View File

@ -6,27 +6,59 @@ import { __ } from '@wordpress/i18n';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import LightBulb from './assets/images/loader-lightbulb.svg'; import LightBulbImage from './assets/images/loader-lightbulb.svg';
import DevelopingImage from './assets/images/loader-developing.svg';
import LayoutImage from './assets/images/loader-layout.svg';
import { Stages } from './pages/Loader'; import { Stages } from './pages/Loader';
const LightbulbStage = {
title: __( 'Turning on the lights', 'woocommerce' ),
image: <img src={ LightBulbImage } alt="loader-lightbulb" />,
paragraphs: [
{
label: __( '#FunWooFact: ', 'woocommerce' ),
text: __(
'The Woo team is made up of over 350 talented individuals, distributed across 30+ countries.',
'woocommerce'
),
},
],
};
const LayoutStage = {
title: __( "Extending your store's capabilities", 'woocommerce' ),
image: <img src={ LayoutImage } alt="loader-lightbulb" />,
paragraphs: [
{
label: __( '#FunWooFact: ', 'woocommerce' ),
text: __(
'#FunWooFact: Did you know that Woo powers almost 4 million stores worldwide? Youre in good company.',
'woocommerce'
),
},
],
};
const DevelopingStage = {
title: __( "Woo! Let's get your features ready", 'woocommerce' ),
image: <img src={ DevelopingImage } alt="loader-developng" />,
paragraphs: [
{
label: __( '#FunWooFact: ', 'woocommerce' ),
text: __(
'Did you know that Woo was founded by two South Africans and a Norwegian? Here are three alternative ways to say “store” in those countries Winkel, ivenkile, and butikk.',
'woocommerce'
),
},
],
};
export const getLoaderStageMeta = ( key: string ): Stages => { export const getLoaderStageMeta = ( key: string ): Stages => {
switch ( key ) { switch ( key ) {
case 'plugins':
return [ DevelopingStage, LayoutStage, LightbulbStage ];
case 'default': case 'default':
default: default:
return [ return [ LightbulbStage ];
{
title: __( 'Turning on the lights', 'woocommerce' ),
image: <img src={ LightBulb } alt="loader-lightbulb" />,
paragraphs: [
{
label: __( '#FunWooFact: ', 'woocommerce' ),
text: __(
'The Woo team is made up of over 350 talented individuals, distributed across 30+ countries.',
'woocommerce'
),
},
],
},
];
} }
}; };

View File

@ -1,3 +1,4 @@
/* eslint-disable xstate/no-inline-implementation */
/** /**
* External dependencies * External dependencies
*/ */
@ -5,7 +6,6 @@ import { createMachine, assign, DoneInvokeEvent, actions, spawn } from 'xstate';
import { useMachine } from '@xstate/react'; import { useMachine } from '@xstate/react';
import { useEffect, useMemo } from '@wordpress/element'; import { useEffect, useMemo } from '@wordpress/element';
import { resolveSelect, dispatch } from '@wordpress/data'; import { resolveSelect, dispatch } from '@wordpress/data';
import { navigateTo, getNewPath } from '@woocommerce/navigation';
import { import {
ExtensionList, ExtensionList,
OPTIONS_STORE_NAME, OPTIONS_STORE_NAME,
@ -32,9 +32,15 @@ import { BusinessInfo } from './pages/BusinessInfo';
import { BusinessLocation } from './pages/BusinessLocation'; import { BusinessLocation } from './pages/BusinessLocation';
import { getCountryStateOptions } from './services/country'; import { getCountryStateOptions } from './services/country';
import { Loader } from './pages/Loader'; import { Loader } from './pages/Loader';
import { Extensions } from './pages/Extensions'; import { Plugins } from './pages/Plugins';
import { getPluginTrackKey, getTimeFrame } from '~/utils';
import './style.scss'; import './style.scss';
import {
InstallationCompletedResult,
InstallAndActivatePlugins,
InstalledPlugin,
PluginInstallError,
} from './services/installAndActivatePlugins';
// TODO: Typescript support can be improved, but for now lets write the types ourselves // TODO: Typescript support can be improved, but for now lets write the types ourselves
// https://stately.ai/blog/introducing-typescript-typegen-for-xstate // https://stately.ai/blog/introducing-typescript-typegen-for-xstate
@ -74,10 +80,10 @@ export type BusinessLocationEvent = {
}; };
}; };
export type ExtensionsEvent = { export type PluginsInstallationRequestedEvent = {
type: 'EXTENSIONS_COMPLETED'; type: 'PLUGINS_INSTALLATION_REQUESTED';
payload: { payload: {
extensionsSelected: CoreProfilerStateMachineContext[ 'extensionsSelected' ]; plugins: CoreProfilerStateMachineContext[ 'pluginsSelected' ];
}; };
}; };
@ -89,6 +95,31 @@ export type OnboardingProfile = {
skip?: boolean; skip?: boolean;
}; };
export type PluginsPageSkippedEvent = {
type: 'PLUGINS_PAGE_SKIPPED';
};
export type PluginInstalledAndActivatedEvent = {
type: 'PLUGIN_INSTALLED_AND_ACTIVATED';
payload: {
pluginsCount: number;
installedPluginIndex: number;
};
};
export type PluginsInstallationCompletedEvent = {
type: 'PLUGINS_INSTALLATION_COMPLETED';
payload: {
installationCompletedResult: InstallationCompletedResult;
};
};
export type PluginsInstallationCompletedWithErrorsEvent = {
type: 'PLUGINS_INSTALLATION_COMPLETED_WITH_ERRORS';
payload: {
errors: PluginInstallError[];
};
};
export type CoreProfilerStateMachineContext = { export type CoreProfilerStateMachineContext = {
optInDataSharing: boolean; optInDataSharing: boolean;
userProfile: { userProfile: {
@ -100,8 +131,9 @@ export type CoreProfilerStateMachineContext = {
geolocatedLocation: { geolocatedLocation: {
location: string; location: string;
}; };
extensionsAvailable: ExtensionList[ 'plugins' ] | []; pluginsAvailable: ExtensionList[ 'plugins' ] | [];
extensionsSelected: string[]; // extension slugs pluginsSelected: string[]; // extension slugs
pluginsInstallationErrors: PluginInstallError[];
businessInfo: { foo?: { bar: 'qux' }; location: string }; businessInfo: { foo?: { bar: 'qux' }; location: string };
countries: { [ key: string ]: string }; countries: { [ key: string ]: string };
loader: { loader: {
@ -175,7 +207,10 @@ const handleOnboardingProfileOption = assign( {
} ); } );
const redirectToWooHome = () => { const redirectToWooHome = () => {
navigateTo( { url: getNewPath( {}, '/', {} ) } ); /**
* @todo replace with navigateTo
*/
window.location.href = '/wp-admin/admin.php?page=wc-admin';
}; };
const recordTracksIntroCompleted = () => { const recordTracksIntroCompleted = () => {
@ -203,6 +238,13 @@ const recordTracksUserProfileViewed = () => {
} ); } );
}; };
const recordTracksPluginsViewed = () => {
recordEvent( 'storeprofiler_step_view', {
step: 'plugins',
wc_version: getSetting( 'wcVersion' ),
} );
};
const recordTracksUserProfileCompleted = ( const recordTracksUserProfileCompleted = (
_context: CoreProfilerStateMachineContext, _context: CoreProfilerStateMachineContext,
event: Extract< UserProfileEvent, { type: 'USER_PROFILE_COMPLETED' } > event: Extract< UserProfileEvent, { type: 'USER_PROFILE_COMPLETED' } >
@ -225,6 +267,10 @@ const recordTracksUserProfileSkipped = () => {
recordEvent( 'storeprofiler_user_profile_skip' ); recordEvent( 'storeprofiler_user_profile_skip' );
}; };
const recordTracksPluginsSkipped = () => {
recordEvent( 'storeprofiler_plugins_skip' );
};
const recordTracksSkipBusinessLocationViewed = () => { const recordTracksSkipBusinessLocationViewed = () => {
recordEvent( 'storeprofiler_step_view', { recordEvent( 'storeprofiler_step_view', {
step: 'skip_business_location', step: 'skip_business_location',
@ -299,7 +345,7 @@ const assignOptInDataSharing = assign( {
/** /**
* Prefetch it so that @wp/data caches it and there won't be a loading delay when its used * Prefetch it so that @wp/data caches it and there won't be a loading delay when its used
*/ */
const preFetchGetExtensions = assign( { const preFetchGetPlugins = assign( {
extensionsRef: () => extensionsRef: () =>
spawn( spawn(
resolveSelect( ONBOARDING_STORE_NAME ).getFreeExtensions(), resolveSelect( ONBOARDING_STORE_NAME ).getFreeExtensions(),
@ -307,33 +353,39 @@ const preFetchGetExtensions = assign( {
), ),
} ); } );
const getExtensions = async () => { const getPlugins = async () => {
dispatch( ONBOARDING_STORE_NAME ).invalidateResolution(
'getFreeExtensions'
);
const extensionsBundles = await resolveSelect( const extensionsBundles = await resolveSelect(
ONBOARDING_STORE_NAME ONBOARDING_STORE_NAME
).getFreeExtensions(); ).getFreeExtensions();
return ( return (
extensionsBundles.find( ( bundle ) => bundle.key === 'obw/grow' ) extensionsBundles.find(
?.plugins || [] ( bundle ) => bundle.key === 'obw/core-profiler'
)?.plugins || []
); );
}; };
const handleExtensions = assign( { const handlePlugins = assign( {
extensionsAvailable: ( _context, event: DoneInvokeEvent< Extension[] > ) => pluginsAvailable: ( _context, event: DoneInvokeEvent< Extension[] > ) =>
event.data, event.data,
} ); } );
const coreProfilerMachineActions = { const coreProfilerMachineActions = {
updateTrackingOption, updateTrackingOption,
preFetchGetExtensions, preFetchGetPlugins,
preFetchGetCountries, preFetchGetCountries,
handleTrackingOption, handleTrackingOption,
handleExtensions, handlePlugins,
recordTracksIntroCompleted, recordTracksIntroCompleted,
recordTracksIntroSkipped, recordTracksIntroSkipped,
recordTracksIntroViewed, recordTracksIntroViewed,
recordTracksUserProfileCompleted, recordTracksUserProfileCompleted,
recordTracksUserProfileSkipped, recordTracksUserProfileSkipped,
recordTracksUserProfileViewed, recordTracksUserProfileViewed,
recordTracksPluginsViewed,
recordTracksPluginsSkipped,
recordTracksSkipBusinessLocationViewed, recordTracksSkipBusinessLocationViewed,
recordTracksSkipBusinessLocationCompleted, recordTracksSkipBusinessLocationCompleted,
assignOptInDataSharing, assignOptInDataSharing,
@ -345,8 +397,8 @@ const coreProfilerMachineActions = {
const coreProfilerMachineServices = { const coreProfilerMachineServices = {
getAllowTrackingOption, getAllowTrackingOption,
getCountries, getCountries,
getExtensions,
getOnboardingProfileOption, getOnboardingProfileOption,
getPlugins,
}; };
export const coreProfilerStateMachineDefinition = createMachine( { export const coreProfilerStateMachineDefinition = createMachine( {
id: 'coreProfiler', id: 'coreProfiler',
@ -359,8 +411,9 @@ export const coreProfilerStateMachineDefinition = createMachine( {
userProfile: { skipped: true }, userProfile: { skipped: true },
geolocatedLocation: { location: 'US:CA' }, geolocatedLocation: { location: 'US:CA' },
businessInfo: { location: 'US:CA' }, businessInfo: { location: 'US:CA' },
extensionsAvailable: [], pluginsAvailable: [],
extensionsSelected: [], pluginsSelected: [],
pluginsInstallationErrors: [],
countries: {}, countries: {},
loader: {}, loader: {},
} as CoreProfilerStateMachineContext, } as CoreProfilerStateMachineContext,
@ -371,7 +424,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
target: 'introOptIn', target: 'introOptIn',
}, },
}, },
entry: [ 'preFetchGetExtensions', 'preFetchGetCountries' ], entry: [ 'preFetchGetPlugins', 'preFetchGetCountries' ],
invoke: [ invoke: [
{ {
src: 'getAllowTrackingOption', src: 'getAllowTrackingOption',
@ -507,7 +560,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
businessInfo: { businessInfo: {
on: { on: {
BUSINESS_INFO_COMPLETED: { BUSINESS_INFO_COMPLETED: {
target: 'preExtensions', target: 'prePlugins',
actions: [ actions: [
assign( { assign( {
businessInfo: ( businessInfo: (
@ -616,32 +669,209 @@ export const coreProfilerStateMachineDefinition = createMachine( {
component: Loader, component: Loader,
}, },
}, },
preExtensions: { prePlugins: {
invoke: { invoke: {
src: 'getExtensions', src: 'getPlugins',
onDone: [ onDone: [
{ target: 'extensions', actions: 'handleExtensions' }, {
target: 'pluginsSkipped',
cond: ( context, event ) => event.data.length === 0,
},
{ target: 'plugins', actions: 'handlePlugins' },
], ],
}, },
// add exit action to filter the extensions using a custom function here and assign it to context.extensionsAvailable // add exit action to filter the extensions using a custom function here and assign it to context.extensionsAvailable
exit: assign( { exit: assign( {
extensionsAvailable: ( context ) => { pluginsAvailable: ( context ) => {
return context.extensionsAvailable.filter( () => true ); return context.pluginsAvailable.filter( () => true );
}, // TODO : define an extensible filter function here }, // TODO : define an extensible filter function here
} ), } ),
meta: { meta: {
progress: 70, progress: 70,
}, },
}, },
extensions: { pluginsSkipped: {
entry: assign( {
loader: {
progress: 80,
},
} ),
invoke: {
src: () => {
dispatch( ONBOARDING_STORE_NAME ).updateProfileItems( {
plugins_page_skipped: true,
completed: true,
} );
return promiseDelay( 3000 );
},
onDone: {
actions: [ 'redirectToWooHome' ],
},
},
meta: {
component: Loader,
},
},
plugins: {
entry: [ 'recordTracksPluginsViewed' ],
on: { on: {
EXTENSIONS_COMPLETED: { PLUGINS_PAGE_SKIPPED: {
target: 'settingUpStore', actions: [ 'recordTracksPluginsSkipped' ],
target: 'pluginsSkipped',
},
PLUGINS_INSTALLATION_REQUESTED: {
target: 'installPlugins',
actions: [
assign( {
pluginsSelected: (
_context,
event: PluginsInstallationRequestedEvent
) => event.payload.plugins,
} ),
],
}, },
}, },
meta: { meta: {
progress: 80, progress: 80,
component: Extensions, component: Plugins,
},
},
postPluginInstallation: {
invoke: {
src: async ( _context, event ) => {
return await dispatch(
ONBOARDING_STORE_NAME
).updateProfileItems( {
business_extensions:
event.payload.installationCompletedResult.installedPlugins.map(
( extension: InstalledPlugin ) =>
extension.plugin
),
completed: true,
} );
},
onDone: {
actions: 'redirectToWooHome',
},
},
meta: {
component: Loader,
progress: 100,
},
},
installPlugins: {
on: {
PLUGIN_INSTALLED_AND_ACTIVATED: {
actions: [
assign( {
loader: (
_context,
event: PluginInstalledAndActivatedEvent
) => {
const progress = Math.round(
( event.payload.installedPluginIndex /
event.payload.pluginsCount ) *
100
);
let stageIndex = 0;
if ( progress > 30 ) {
stageIndex = 1;
} else if ( progress > 60 ) {
stageIndex = 2;
}
return {
useStages: 'plugins',
progress,
stageIndex,
};
},
} ),
],
},
PLUGINS_INSTALLATION_COMPLETED_WITH_ERRORS: {
target: 'prePlugins',
actions: [
assign( {
pluginsInstallationErrors: ( _context, event ) =>
event.payload.errors,
} ),
( _context, event ) => {
recordEvent(
'storeprofiler_store_extensions_installed_and_activated',
{
success: false,
failed_extensions: event.payload.errors.map(
( error: PluginInstallError ) =>
getPluginTrackKey( error.plugin )
),
}
);
},
],
},
PLUGINS_INSTALLATION_COMPLETED: {
target: 'postPluginInstallation',
actions: [
( _context, event ) => {
const installationCompletedResult =
event.payload.installationCompletedResult;
const trackData: {
success: boolean;
installed_extensions: string[];
total_time: string;
[ key: string ]:
| number
| boolean
| string
| string[];
} = {
success: true,
installed_extensions:
installationCompletedResult.installedPlugins.map(
( installedPlugin: InstalledPlugin ) =>
getPluginTrackKey(
installedPlugin.plugin
)
),
total_time: getTimeFrame(
installationCompletedResult.totalTime
),
};
for ( const installedPlugin of installationCompletedResult.installedPlugins ) {
trackData[
'install_time_' +
getPluginTrackKey(
installedPlugin.plugin
)
] = getTimeFrame( installedPlugin.installTime );
}
recordEvent(
'storeprofiler_store_extensions_installed_and_activated',
trackData
);
},
],
},
},
entry: [
assign( {
loader: {
progress: 10,
useStages: 'plugins',
},
} ),
],
invoke: {
src: InstallAndActivatePlugins,
},
meta: {
component: Loader,
}, },
}, },
settingUpStore: {}, settingUpStore: {},

View File

@ -1,31 +0,0 @@
/**
* Internal dependencies
*/
import { CoreProfilerStateMachineContext, ExtensionsEvent } from '../index';
export const Extensions = ( {
context,
sendEvent,
}: {
context: CoreProfilerStateMachineContext;
sendEvent: ( payload: ExtensionsEvent ) => void;
} ) => {
return (
<>
<div>Extensions</div>
<div>{ JSON.stringify( context.extensionsAvailable ) }</div>
<button
onClick={ () =>
sendEvent( {
type: 'EXTENSIONS_COMPLETED',
payload: {
extensionsSelected: [ 'woocommerce-payments' ],
},
} )
}
>
Next
</button>
</>
);
};

View File

@ -0,0 +1,213 @@
/**
* External dependencies
*/
import { __, sprintf, _n } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import interpolateComponents from '@automattic/interpolate-components';
import { Link } from '@woocommerce/components';
import { Extension, ExtensionList } from '@woocommerce/data';
import { useState } from 'react';
/**
* Internal dependencies
*/
import { CoreProfilerStateMachineContext } from '../index';
import { PluginsInstallationRequestedEvent, PluginsPageSkippedEvent } from '..';
import { Heading } from '../components/heading/heading';
import { Navigation } from '../components/navigation/navigation';
import { PluginCard } from '../components/plugin-card/plugin-card';
import { getAdminSetting } from '~/utils/admin-settings';
const locale = ( getAdminSetting( 'locale' )?.siteLocale || 'en_US' ).replace(
'_',
'-'
);
const joinWithAnd = ( items: string[] ) => {
return new Intl.ListFormat( locale, {
style: 'long',
type: 'conjunction',
} ).formatToParts( items );
};
export const Plugins = ( {
context,
navigationProgress,
sendEvent,
}: {
context: CoreProfilerStateMachineContext;
sendEvent: (
payload: PluginsInstallationRequestedEvent | PluginsPageSkippedEvent
) => void;
navigationProgress: number;
} ) => {
const [ selectedPlugins, setSelectedPlugins ] = useState<
ExtensionList[ 'plugins' ]
>( context.pluginsAvailable.filter( ( plugin ) => ! plugin.is_installed ) );
const setSelectedPlugin = ( plugin: Extension ) => {
setSelectedPlugins(
selectedPlugins.some( ( item ) => item.key === plugin.key )
? selectedPlugins.filter( ( item ) => item.key !== plugin.key )
: [ ...selectedPlugins, plugin ]
);
};
const skipPluginsPage = () => {
return sendEvent( {
type: 'PLUGINS_PAGE_SKIPPED',
} );
};
const submitInstallationRequest = () => {
return sendEvent( {
type: 'PLUGINS_INSTALLATION_REQUESTED',
payload: {
plugins: selectedPlugins.map( ( plugin ) =>
plugin.key.replace( ':alt', '' )
),
},
} );
};
const composeListFormatParts = ( part: {
type: string;
value: string;
} ) => {
if ( part.type === 'element' ) {
return '{{span}}' + part.value + '{{/span}}';
}
return part.value;
};
const errorMessage = context.pluginsInstallationErrors.length
? interpolateComponents( {
mixedString: sprintf(
// Translators: %s is a list of plugins that does not need to be translated
__(
'Oops! We encountered a problem while installing %s. {{link}}Please try again{{/link}}.',
'woocommerce'
),
joinWithAnd(
context.pluginsInstallationErrors.map(
( error ) => error.plugin
)
)
.map( composeListFormatParts )
.join( '' )
),
components: {
span: <span />,
link: (
<Button isLink onClick={ submitInstallationRequest } />
),
},
} )
: null;
const pluginsWithAgreement = selectedPlugins.filter( ( plugin ) =>
[
'jetpack',
'woocommerce-services:shipping',
'woocommerce-services:tax',
].includes( plugin.key )
);
return (
<div
className="woocommerce-profiler-plugins"
data-testid="core-profiler-plugins"
>
<Navigation
percentage={ navigationProgress }
onSkip={ skipPluginsPage }
/>
<div className="woocommerce-profiler-page__content woocommerce-profiler-plugins__content">
<Heading
className="woocommerce-profiler__stepper-heading"
title={ __(
'Get a boost with our free features',
'woocommerce'
) }
subTitle={ __(
'Enhance your store by installing these free business features. No commitment required you can remove them at any time.',
'woocommerce'
) }
/>
{ errorMessage && (
<p className="plugin-error">{ errorMessage }</p>
) }
<div className="woocommerce-profiler-plugins__list">
{ context.pluginsAvailable.map( ( plugin ) => {
return (
<PluginCard
key={ `checkbox-control-${ plugin.key }` }
installed={ plugin.is_installed }
onChange={ () => {
setSelectedPlugin( plugin );
} }
checked={
selectedPlugins.filter(
( item ) => item.key === plugin.key
).length > 0
}
icon={
plugin.image_url ? (
<img
src={ plugin.image_url }
alt={ plugin.key }
/>
) : null
}
title={ plugin.name }
description={ plugin.description }
/>
);
} ) }
</div>
<div className="woocommerce-profiler-plugins-continue-button-container">
<Button
className="woocommerce-profiler-plugins-continue-button"
variant="primary"
onClick={
selectedPlugins.length
? submitInstallationRequest
: skipPluginsPage
}
>
{ __( 'Continue', 'woocommerce' ) }
</Button>
</div>
{ pluginsWithAgreement.length > 0 && (
<p className="woocommerce-profiler-plugins-jetpack-agreement">
{ interpolateComponents( {
mixedString: sprintf(
/* translators: %s: a list of plugins, e.g. Jetpack */
_n(
'By installing %s plugin for free you agree to our {{link}}Terms of Service{{/link}}.',
'By installing %s plugins for free you agree to our {{link}}Terms of Service{{/link}}.',
pluginsWithAgreement.length,
'woocommerce'
),
joinWithAnd(
pluginsWithAgreement.map(
( plugin ) => plugin.name
)
)
.map( composeListFormatParts )
.join( '' )
),
components: {
span: <span />,
link: (
<Link
href="https://wordpress.com/tos/"
target="_blank"
type="external"
/>
),
},
} ) }
</p>
) }
</div>
</div>
);
};

View File

@ -0,0 +1,168 @@
/**
* External dependencies
*/
import { PLUGINS_STORE_NAME, PluginNames } from '@woocommerce/data';
import { dispatch } from '@wordpress/data';
import { differenceWith } from 'lodash';
/**
* Internal dependencies
*/
import {
PluginInstalledAndActivatedEvent,
PluginsInstallationCompletedEvent,
PluginsInstallationCompletedWithErrorsEvent,
CoreProfilerStateMachineContext,
} from '..';
import { getPluginSlug } from '~/utils';
export type InstalledPlugin = {
plugin: string;
installTime: number;
};
export type InstallationCompletedResult = {
installedPlugins: InstalledPlugin[];
totalTime: number;
};
export type PluginInstallError = {
plugin: string;
error: string;
};
const createInstallationCompletedWithErrorsEvent = (
errors: PluginInstallError[]
): PluginsInstallationCompletedWithErrorsEvent => ( {
type: 'PLUGINS_INSTALLATION_COMPLETED_WITH_ERRORS',
payload: {
errors,
},
} );
const createInstallationCompletedEvent = (
installationCompletedResult: InstallationCompletedResult
): PluginsInstallationCompletedEvent => ( {
type: 'PLUGINS_INSTALLATION_COMPLETED',
payload: {
installationCompletedResult,
},
} );
const createPluginInstalledAndActivatedEvent = (
pluginsCount: number,
installedPluginIndex: number
): PluginInstalledAndActivatedEvent => ( {
type: 'PLUGIN_INSTALLED_AND_ACTIVATED',
payload: {
pluginsCount,
installedPluginIndex,
},
} );
export const InstallAndActivatePlugins =
( context: CoreProfilerStateMachineContext ) =>
async (
send: (
event:
| PluginInstalledAndActivatedEvent
| PluginsInstallationCompletedEvent
| PluginsInstallationCompletedWithErrorsEvent
) => void
) => {
let continueInstallation = true;
const errors: PluginInstallError[] = [];
const installationCompletedResult: InstallationCompletedResult = {
installedPlugins: [],
totalTime: 0,
};
const installationStartTime = window.performance.now();
const setInstallationCompletedTime = () => {
installationCompletedResult.totalTime =
window.performance.now() - installationStartTime;
};
const handleInstallationCompleted = () => {
if ( errors.length ) {
return send(
createInstallationCompletedWithErrorsEvent( errors )
);
}
setInstallationCompletedTime();
send(
createInstallationCompletedEvent( installationCompletedResult )
);
};
const handlePluginInstalledAndActivated = (
installedPluginIndex: number
) => {
send(
createPluginInstalledAndActivatedEvent(
context.pluginsSelected.length,
installedPluginIndex + 1
)
);
};
const handlePluginInstallError = ( plugin: string, error: unknown ) => {
errors.push( {
plugin,
error: error instanceof Error ? error.message : String( error ),
} );
};
const handlePluginInstallation = async (
installedPluginIndex: number
) => {
// Set by timer when it's up
if ( ! continueInstallation ) {
return;
}
const plugin = getPluginSlug(
context.pluginsSelected[ installedPluginIndex ]
);
try {
const response = await dispatch(
PLUGINS_STORE_NAME
).installAndActivatePlugins( [ plugin ] );
installationCompletedResult.installedPlugins.push( {
plugin,
installTime: response.data?.install_time?.[ plugin ] || 0,
} );
handlePluginInstalledAndActivated( installedPluginIndex );
} catch ( error ) {
handlePluginInstallError( plugin, error );
}
};
const handleTimerExpired = async () => {
continueInstallation = false;
const remainingPlugins = differenceWith(
context.pluginsSelected,
installationCompletedResult.installedPlugins.map(
( plugin ) => plugin.plugin
)
);
await dispatch( PLUGINS_STORE_NAME ).installPlugins(
remainingPlugins as PluginNames[],
true
);
handleInstallationCompleted();
};
const timer = setTimeout( handleTimerExpired, 1000 * 30 );
for ( let index = 0; index < context.pluginsSelected.length; index++ ) {
await handlePluginInstallation( index );
}
clearTimeout( timer );
handleInstallationCompleted();
};

View File

@ -1,3 +1,9 @@
.woocommerce-layout .woocommerce-layout__main {
@include breakpoint( '<782px' ) {
padding-top: 0 !important;
}
}
.woocommerce-profile-wizard__body { .woocommerce-profile-wizard__body {
background-color: #fff; background-color: #fff;
@ -208,44 +214,76 @@
} }
} }
} }
// Business location page // Business location page
.woocommerce-profiler-business-location { .woocommerce-profiler-select-control__country {
display: flex; max-width: 400px;
flex-direction: column; margin: auto auto 32px auto;
.woocommerce-profiler-business-location__content { .woocommerce-select-control__option {
max-width: 550px; font-size: 13px;
width: 100%; &:hover {
background: #eff2fd;
}
}
.woocommerce-select-control__listbox {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
border: 1px solid #ccc;
margin-top: 8px;
}
.woocommerce-select-control__control {
height: 40px;
padding: 12px;
border: 1px solid #bbb;
label,
input {
font-size: 13px;
color: var(--wp-components-color-foreground, #757575);
}
&.is-active {
border: 1px solid #3858e9;
}
}
#woocommerce-select-control__listbox-0 {
top: 40px !important;
}
.woocommerce-profiler-business-location {
display: flex; display: flex;
align-self: center; flex-direction: column;
.woocommerce-profiler-business-location__content {
& > div { max-width: 550px;
width: 100%; width: 100%;
} display: flex;
align-self: center;
.components-base-control__field { & > div {
height: 40px; width: 100%;
} }
.woocommerce-select-control__control-icon { .components-base-control__field {
display: none; height: 40px;
} }
.woocommerce-select-control__control.is-active { .woocommerce-select-control__control-icon {
.components-base-control__label {
display: none; display: none;
} }
}
.woocommerce-select-control.is-searchable .woocommerce-select-control__control.is-active {
.woocommerce-select-control__control-input { .components-base-control__label {
margin: 0; display: none;
padding: 0; }
} }
.woocommerce-select-control.is-searchable .woocommerce-select-control.is-searchable
.components-base-control__label { .woocommerce-select-control__control-input {
left: 13px; margin: 0;
padding: 0;
}
.woocommerce-select-control.is-searchable
.components-base-control__label {
left: 13px;
}
} }
} }
} }
@ -344,3 +382,78 @@
margin-top: 20px; margin-top: 20px;
} }
} }
.woocommerce-profiler-plugins {
display: flex;
flex-direction: column;
.woocommerce-profiler-plugins__content {
display: flex;
align-items: center;
align-self: center;
max-width: 1000px;
width: 100%;
}
.woocommerce-profiler-heading {
max-width: 615px;
margin: 58px 0 0 0;
}
.woocommerce-profiler-heading__subtitle {
margin: 0 0 48px 0 !important;
@include breakpoint( '<782px' ) {
margin-top: 12px !important;
}
}
.woocommerce-profiler-plugins__list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 16px;
flex: 50%;
}
.woocommerce-profiler-plugins-continue-button {
width: 200px;
height: 48px;
margin-top: 28px;
text-align: center;
display: block;
}
.plugin-error {
width: 100%;
border-left: 4px solid #cc1818;
background-color: #fce2e4;
padding: 12px;
margin: 0 0 28px 0;
color: #1e1e1e;
button {
text-decoration: none;
}
span {
font-weight: bold;
}
}
.woocommerce-profiler-plugins-jetpack-agreement {
color: $gray-700;
font-size: 12px;
a {
color: inherit;
}
}
.woocommerce-profiler-plugins-continue-button-container {
@include breakpoint( '<782px' ) {
position: absolute;
bottom: 20px;
padding: 0 20px;
width: 100%;
}
.woocommerce-profiler-plugins-continue-button {
@include breakpoint( '<782px' ) {
width: 100%;
}
}
}
}

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add plugins page to the core profiler