diff --git a/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php b/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php index c7f5ec131..1d100cdda 100644 --- a/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php +++ b/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php @@ -16,6 +16,8 @@ /** * Class PartnerReferrals + * + * @see https://developer.paypal.com/docs/api/partner-referrals/v2/ */ class PartnerReferrals { diff --git a/modules/ppcp-settings/images/icon-button-payment-method-multibanco.svg b/modules/ppcp-settings/images/icon-button-payment-method-multibanco.svg new file mode 100644 index 000000000..9d423223c --- /dev/null +++ b/modules/ppcp-settings/images/icon-button-payment-method-multibanco.svg @@ -0,0 +1 @@ +Logo_Multibanco diff --git a/modules/ppcp-settings/images/icon-button-payment-method-mybank.svg b/modules/ppcp-settings/images/icon-button-payment-method-mybank.svg new file mode 100644 index 000000000..82dd40ca4 --- /dev/null +++ b/modules/ppcp-settings/images/icon-button-payment-method-mybank.svg @@ -0,0 +1,28 @@ + + + + MyBank + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/ppcp-settings/images/icon-button-payment-method-oxxo.svg b/modules/ppcp-settings/images/icon-button-payment-method-oxxo.svg new file mode 100644 index 000000000..4f69e152d --- /dev/null +++ b/modules/ppcp-settings/images/icon-button-payment-method-oxxo.svg @@ -0,0 +1,18 @@ + + + + logo OXXO + Created with Sketch. + + + + + + + + + + + + + diff --git a/modules/ppcp-settings/images/icon-button-payment-method-przelewy24.svg b/modules/ppcp-settings/images/icon-button-payment-method-przelewy24.svg new file mode 100644 index 000000000..3ab7a31be --- /dev/null +++ b/modules/ppcp-settings/images/icon-button-payment-method-przelewy24.svg @@ -0,0 +1,38 @@ + + + + logo P24 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/ppcp-settings/images/icon-button-payment-method-ratepay.svg b/modules/ppcp-settings/images/icon-button-payment-method-ratepay.svg new file mode 100644 index 000000000..f0da1b689 --- /dev/null +++ b/modules/ppcp-settings/images/icon-button-payment-method-ratepay.svg @@ -0,0 +1,3 @@ + + + diff --git a/modules/ppcp-settings/images/icon-button-payment-method-trustly.svg b/modules/ppcp-settings/images/icon-button-payment-method-trustly.svg new file mode 100644 index 000000000..85bfacbe0 --- /dev/null +++ b/modules/ppcp-settings/images/icon-button-payment-method-trustly.svg @@ -0,0 +1 @@ + diff --git a/modules/ppcp-settings/resources/css/_variables.scss b/modules/ppcp-settings/resources/css/_variables.scss index 613403b67..10f427ea9 100644 --- a/modules/ppcp-settings/resources/css/_variables.scss +++ b/modules/ppcp-settings/resources/css/_variables.scss @@ -10,10 +10,12 @@ $color-gray-500: #BBBBBB; $color-gray-400: #CCCCCC; $color-gray-300: #EBEBEB; $color-gray-200: #E0E0E0; +$color-gray-100: #F0F0F0; $color-gray: #646970; $color-text-tertiary: #505050; $color-text-text: #070707; -$color-border:#AEAEAE; +$color-border: #AEAEAE; +$color-divider: #F0F0F0; $shadow-card: 0 3px 6px 0 rgba(0, 0, 0, 0.15); $shadow-selection-box: 0 2px 4px 0 rgba(0, 0, 0, 0.1); @@ -24,10 +26,32 @@ $max-width-onboarding: 1024px; $max-width-onboarding-content: 500px; $max-width-settings: 938px; +$card-vertical-gap: 48px; + +/* define custom theming options */ + +:root { + --ppcp-color-app-bg: #{$color-white}; +} + #ppcp-settings-container { --max-width-settings: #{$max-width-settings}; --max-width-onboarding: #{$max-width-onboarding}; --max-width-onboarding-content: #{$max-width-onboarding-content}; --max-container-width: var(--max-width-settings); + + --color-black: #{$color-black}; + --color-white: #{$color-white}; + --color-blueberry: #{$color-blueberry}; + --color-gray-900: #{$color-gray-900}; + --color-gray-800: #{$color-gray-800}; + --color-gray-700: #{$color-gray-700}; + --color-gray-600: #{$color-gray-600}; + --color-gray-500: #{$color-gray-500}; + --color-gray-400: #{$color-gray-400}; + --color-gray-300: #{$color-gray-300}; + --color-gray-200: #{$color-gray-200}; + --color-gray-100: #{$color-gray-100}; + --color-gradient-dark: #{$color-gradient-dark}; } diff --git a/modules/ppcp-settings/resources/css/components/_app.scss b/modules/ppcp-settings/resources/css/components/_app.scss new file mode 100644 index 000000000..7e69cbada --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/_app.scss @@ -0,0 +1,22 @@ +/** + * Global app-level styles + */ + +.ppcp-r-app.loading { + height: 400px; + width: 400px; + position: absolute; + left: 50%; + transform: translate(-50%, 0); + text-align: center; + + .ppcp-r-spinner-overlay { + display: flex; + flex-direction: column; + justify-content: center; + } + + .ppcp-r-spinner-overlay__message { + transform: translate(0, 32px) + } +} diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_accordion-section.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_accordion-section.scss index 28ac713ce..d4894abd8 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_accordion-section.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_accordion-section.scss @@ -2,26 +2,27 @@ margin-left: auto; margin-right: auto; - &--title { + &__toggler { + display: block; + cursor: pointer; + + background: transparent; + border: 0; + box-shadow: none; + padding: 0; + margin: 24px auto; + } + + &__title-wrapper { @include font(14, 32, 450); color: $color-gray-900; display: flex; align-items: center; gap: 16px; - margin: 24px auto; - border: 0; - background: transparent; - cursor: pointer; } - &--content { + &__content { margin: 24px 0 0; } - - &.ppcp--is-open { - .ppcp-r-accordion--icon { - transform: rotate(180deg); - } - } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss index 427b4b1fb..74fb531ee 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss @@ -33,5 +33,20 @@ margin: 6px 0px 0px 0px; width: fit-content; } + + @media screen and (max-width: 480px) { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + flex-direction: column; + + .ppcp-r-badge-box__title-text:not(:empty) + .ppcp-r-badge-box__title-image-badge { + margin: 0px; + img:first-of-type { + margin: 0px; + } + } + } } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_busy-state.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_busy-state.scss new file mode 100644 index 000000000..4254320aa --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_busy-state.scss @@ -0,0 +1,10 @@ +.ppcp-r-busy-wrapper { + position: relative; + + &.ppcp--is-loading { + pointer-events: none; + user-select: none; + + --spinner-overlay-color: #fff4; + } +} diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss index 0afb0a304..558ccaaf2 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss @@ -1,47 +1,102 @@ +%button-style-default { + background-color: var(--button-background); + color: var(--button-color); + box-shadow: inset 0 0 0 1px var(--button-border-color); +} + +%button-style-hover { + background-color: var(--button-hover-background); + color: var(--button-hover-color); + box-shadow: inset 0 0 0 1px var(--button-hover-border-color); +} + +%button-style-disabled { + background-color: var(--button-disabled-background); + color: var(--button-disabled-color); + box-shadow: inset 0 0 0 1px var(--button-disabled-border-color); +} + +%button-shape-pill { + border-radius: 50px; + padding: 15px 32px; + height: auto; +} + button.components-button, a.components-button { - &.is-primary, &.is-secondary { - &:not(:disabled) { - background-color: $color-black; - } + /* default theme */ + --button-color: var(--color-gray-900); + --button-background: transparent; + --button-border-color: transparent; - &:disabled { - color: $color-gray-700; - } + --button-hover-color: var(--button-color); + --button-hover-background: var(--button-background); + --button-hover-border-color: var(--button-border-color); + + --button-disabled-color: var(--color-gray-500); + --button-disabled-background: transparent; + --button-disabled-border-color: transparent; + + /* style the button template */ - border-radius: 50px; - padding: 15px 32px; - height: auto; + &:not(:disabled) { + @extend %button-style-default; + } + + &:hover { + @extend %button-style-hover; + } + + &:disabled { + @extend %button-style-disabled; + } + + /* + ---------------------------------------------- + Customize variants using the theming variables + */ + + &.is-primary, + &.is-secondary { + @extend %button-shape-pill; } &.is-primary { @include font(14, 18, 900); - &:not(:disabled) { - background-color: $color-black; - } + --button-color: #{$color-white}; + --button-background: #{$color-blueberry}; + + --button-disabled-color: #{$color-gray-100}; + --button-disabled-background: #{$color-gray-500}; } - &.is-secondary:not(:disabled) { - border-color: $color-blueberry; - background-color: $color-white; - color: $color-blueberry; + &.is-secondary { + --button-color: #{$color-blueberry}; + --button-background: #{$color-white}; + --button-border-color: #{$color-blueberry}; - &:hover { - background-color: $color-white; - background: none; - } + --button-disabled-color: #{$color-gray-600}; + --button-disabled-background: #{$color-gray-100}; + --button-disabled-border-color: #{$color-gray-400}; } &.is-tertiary { - color: $color-blueberry; - - &:hover { - color: $color-gradient-dark; - } + --button-color: #{$color-blueberry}; + --button-hover-color: #{$color-gradient-dark}; &:focus:not(:disabled) { border: none; box-shadow: none; } } + + &.small-button { + @include small-button; + } +} + +.ppcp--is-loading { + button.components-button, a.components-button { + @extend %button-style-disabled; + } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss index 4b6cadfd8..1a9cf102c 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss @@ -58,13 +58,13 @@ position: relative; label { - @include font(14, 20, 400); + @include font(13, 20, 400); color: $color-gray-800; } } &__radio-description { - @include font(14, 20, 400); + @include font(13, 20, 400); margin: 0; color: $color-gray-800; } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss index 09be265c1..a766433e0 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss @@ -1,64 +1,111 @@ .ppcp-r-navigation-container { - padding: 24px 48px; + position: sticky; + top: var(--wp-admin--admin-bar--height); + z-index: 10; + + padding: 10px 48px; margin: 0 -20px 48px -20px; - border-bottom: 1px solid $color-gray-300; - position: relative; + + box-shadow: 0 -1px 0 0 $color-gray-300 inset; + background: var(--ppcp-color-app-bg); + transition: box-shadow 0.3s; + + --wp-components-color-accent: #{$color-blueberry}; + --color-text: #{$color-gray-900}; + --color-disabled: #CCC; .ppcp-r-navigation { display: flex; justify-content: space-between; align-items: center; + height: 40px; + + .components-button { + @include font(13, 20, 400); + + &.is-primary { + background-color: var(--wp-components-color-accent); + padding: 10px 16px; + justify-content: center; + margin: 0 0 0 12px; + border-radius: 2px; - button.is-primary { - padding: 10px 18px; - justify-content: center; - margin: 0 0 0 12px; - &:not(:disabled) { - background-color: $color-blueberry; + &:disabled { + background-color: var(--color-disabled); + } } - } - button.is-tertiary { - @include font(16, 24, 600); - color: $color-gray-900; - &:hover{ - background-color:none; - background:none; + &.is-link { + color: var(--wp-components-color-accent); + text-decoration: none; + + &:disabled { + color: var(--color-disabled); + } + } + + &.is-title { + @include font(16, 24, 600); + color: var(--color-text); + + .title { + margin-left: 18px; + } + + .big { + @include font(20, 28, 400); + } } } &--left { - &__link { - @include font(20, 28, 400); - color: $color-gray-900; - text-decoration: none; - padding: 0 0 0 18px; - } + align-items: center; + display: inline-flex; } - &--right a{ - @include font(13, 20, 400); - color: $color-blueberry; - text-decoration: none; + &--right { + .is-link { + padding: 10px 16px; + } } &--progress-bar { position: absolute; - bottom: 0px; + bottom: 0; left: 0; - background-color: $color-blueberry; + background-color: var(--wp-components-color-accent); height: 4px; + transition: width 0.3s; } } - @media screen and (max-width: 480px) { - padding: 24px 35px; + &.is-scrolled { + box-shadow: 0 -1px 0 0 $color-gray-300 inset, 0 8px 8px 0 rgba(85, 93, 102, .3); + } + + @media screen and (max-width: 782px) { + padding: 10px 12px; + .ppcp-r-navigation { - flex-wrap: wrap; row-gap: 8px; + white-space: nowrap; + + &--right { + position: absolute; + right: 10px; + z-index: 10; + background: var(--ppcp-color-app-bg); + box-shadow: -5px 0 8px var(--ppcp-color-app-bg); + } &--progress-bar { - display: none; + height: 2px; + } + + .components-button.is-title { + .title { + margin-left: 4px; + } } } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_onboarding-header.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_onboarding-header.scss index d6d8cf4f3..70348532e 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_onboarding-header.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_onboarding-header.scss @@ -31,5 +31,9 @@ @include font(14, 22, 400); margin: 0 20%; text-align: center; + + @media screen and (max-width: 480px) { + margin: 0px; + } } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss index 3ca44193c..c85c5162e 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss @@ -1,75 +1,78 @@ -.ppcp-r-payment-method-item-list { - display: flex; - flex-wrap: wrap; - gap: 16px; -} - -.ppcp-r-payment-method-item { - display: flex; - align-items: flex-start; - width: calc(100% / 3 - 32px / 3); - border: 1px solid $color-gray-300; - padding: 16px; - border-radius: 8px; - min-height: 200px; - - @media screen and (max-width: 767px) { - width: calc(50% - 8px); - } - - @media screen and (max-width: 480px) { - width: 100%; - } - - &__wrap { +.ppcp-r-settings-block__payment-methods { + &.ppcp-r-settings-block { display: flex; - flex-direction: column; - height: 100%; + flex-wrap: wrap; + flex-direction: row; + gap: 16px; } - &__title-wrap { + &__item { display: flex; - align-items: center; - margin: 0 0 8px 0; - gap: 12px; - } + align-items: flex-start; + width: calc(100% / 3 - 32px / 3); + border: 1px solid $color-gray-300; + padding: 16px; + border-radius: 8px; + min-height: 200px; - &__content { - p { - margin: 0; - color: $color-text-tertiary; - @include font(13, 20, 400); + @media screen and (max-width: 767px) { + width: calc(50% - 8px); } - margin: 0 0 12px 0; - } + @media screen and (max-width: 480px) { + width: 100%; + } - &__title { - @include font(13, 20, 500); - color: $color-black; - display: block; - } + &__inner { + display: flex; + flex-direction: column; + height: 100%; + } - &__settings-button { - line-height: 0; - transition: 0.2s ease-out transform; - transform: rotate(0deg); - zoom: 1.005; + &__title-wrapper { + display: flex; + align-items: center; + margin: 0 0 8px 0; + gap: 12px; + } + + &__description { + p { + margin: 0; + color: $color-text-tertiary; + @include font(13, 20, 400); + } - &:hover { - transform: rotate(45deg); - cursor: pointer; + margin: 0 0 12px 0; } - } - button.is-secondary { - @include small-button; - } + &__title { + @include font(13, 20, 500); + color: $color-black; + display: block; + } - &__footer { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: auto; + &__settings { + line-height: 0; + transition: 0.2s ease-out transform; + transform: rotate(0deg); + zoom: 1.005; + + &:hover { + transform: rotate(45deg); + cursor: pointer; + } + } + + button.is-secondary { + @include small-button; + } + + &__footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: auto; + } } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-toggle-block.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-toggle-block.scss index e5157a862..af4d264ad 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-toggle-block.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-toggle-block.scss @@ -31,10 +31,4 @@ &__toggled-content { margin-top: 24px; } - - &.ppcp--is-loading { - pointer-events: none; - - --spinner-overlay-color: #fff4; - } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-wrapper.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-wrapper.scss index dd83011c0..a13df6e77 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-wrapper.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-wrapper.scss @@ -16,6 +16,17 @@ } } + &-settings { + > * { + margin-bottom: $card-vertical-gap; + } + + > *:not(:last-child) { + padding-bottom: $card-vertical-gap; + border-bottom: 1px solid $color-gray-200; + } + } + &-settings-card { @media screen and (min-width: 960px) { display: flex; @@ -26,6 +37,12 @@ padding: 24px; } + &__content-wrapper { + display: flex; + flex-direction: column; + gap: 24px; + } + &__header { display: flex; gap: 18px; @@ -43,21 +60,25 @@ } &__content { + border: 1px solid $color-gray-200; + border-radius: 4px; + padding: 24px; @media screen and (min-width: 960px) { flex: 1; } } &__title { - @include font(16, 24, 600); - color: $color-blueberry; + @include font(13, 24, 600); + color: $color-text-text; margin: 0 0 4px 0; display: block; } + &__description { - @include font(14, 20, 400); - color: $color-gray-800; + @include font(13, 20, 400); + color: $color-text-tertiary; margin: 0; } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss index 2f32b118f..8f5e136e9 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_spinner-overlay.scss @@ -12,5 +12,6 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); + margin: 0; } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_title-badge.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_title-badge.scss index 2abd25541..38429a8f7 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_title-badge.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_title-badge.scss @@ -1,12 +1,11 @@ .ppcp-r-title-badge{ @include font(12, 16, 400); - margin-left:12px; - padding:4px 8px; + padding: 4px 8px; border-radius: 2px; white-space: nowrap; &--positive{ - color:#005C12; - background-color: #EDFAEF; + color: #144722; + background-color: #DAFFE0; } &--negative{ color:#5c0000; diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss index 411d5a987..f6dce1407 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss @@ -20,8 +20,7 @@ justify-content: center; @media screen and (max-width: 480px) { - flex-wrap: wrap; - row-gap: 8px; + display: block; } } } diff --git a/modules/ppcp-settings/resources/css/components/screens/_fullscreen.scss b/modules/ppcp-settings/resources/css/components/screens/_fullscreen.scss new file mode 100644 index 000000000..214cc11d7 --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/screens/_fullscreen.scss @@ -0,0 +1,18 @@ +body:has(.ppcp-r-container--settings), +body:has(.ppcp-r-container--onboarding) { + background-color: var(--ppcp-color-app-bg) !important; + + .woocommerce-layout, + #woocommerce-layout__primary { + padding: 0 !important; + } + + .notice, + .nav-tab-wrapper.woo-nav-tab-wrapper, + .woocommerce-layout__header, + .wrap.woocommerce form > h2, + #screen-meta-links { + display: none !important; + visibility: hidden; + } +} diff --git a/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss b/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss deleted file mode 100644 index 7878ef729..000000000 --- a/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss +++ /dev/null @@ -1,8 +0,0 @@ -body:has(.ppcp-r-container--onboarding) { - background-color: #fff !important; - - .notice, .nav-tab-wrapper.woo-nav-tab-wrapper, .woocommerce-layout__header, .wrap.woocommerce form > h2, #screen-meta-links { - display: none !important; - visibility: hidden; - } -} diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings-global.scss b/modules/ppcp-settings/resources/css/components/screens/_settings-global.scss deleted file mode 100644 index 629d89d76..000000000 --- a/modules/ppcp-settings/resources/css/components/screens/_settings-global.scss +++ /dev/null @@ -1,7 +0,0 @@ -body:has(.ppcp-r-container--settings) { - background-color: #fff !important; - - .notice, .nav-tab-wrapper.woo-nav-tab-wrapper, .woocommerce-layout, .wrap.woocommerce form > h2, #screen-meta-links { - display: none !important; - } -} diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings.scss b/modules/ppcp-settings/resources/css/components/screens/_settings.scss index c3879a3db..b98a5f7e1 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_settings.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_settings.scss @@ -1,3 +1,6 @@ +@import "./settings/block-accordion"; + +// Container and Tab Settings .ppcp-r-tabs.settings, .ppcp-r-container--settings { --max-container-width: var(--max-width-settings); @@ -6,3 +9,527 @@ max-width: var(--max-container-width); } } + +// Todo List and Feature Items +.ppcp-r-tab-overview-todo { + margin: 0 0 48px 0; +} + +.ppcp-r-todo-item { + position: relative; + display: flex; + align-items: center; + gap: 18px; + width: 100%; + + &:not(:last-child) { + border-bottom: 1px solid $color-gray-400; + padding-bottom: 16px; + } + + &:not(:first-child) { + padding-top: 16px; + } + + p { + @include font(14, 20, 400); + } + + &__inner { + position: relative; + display: flex; + align-items: center; + gap: 18px; + } + + &__close { + margin-left: auto; + + &:hover { + cursor: pointer; + color: $color-blueberry; + } + } + + .ppcp-r__checkbox { + .components-flex { + gap: 12px; + } + + label { + @include font(13, 20, 400); + color: $color-blueberry; + } + } + + &__description { + @include font(13, 20, 400); + color: $color-blueberry; + } +} + +.ppcp-r-feature-item { + padding-top: 32px; + border-top: 1px solid $color-gray-400; + + &__title { + @include font(16, 20, 600); + color: $color-black; + display: block; + margin: 0 0 8px 0; + } + + &__description { + @include font(14, 20, 400); + color: $color-gray-800; + margin: 0 0 18px 0; + } + + &:not(:last-child) { + padding-bottom: 32px; + } + + &__buttons { + display: flex; + gap: 18px; + } + + &__notes { + display: flex; + flex-direction: column; + + span { + font-weight: 500; + } + } +} + +// Connection Status +.ppcp-r-connection-status { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + + &__status-status { + margin: 0 0 8px 0; + + strong { + @include font(14, 24, 700); + color: $color-black; + } + } + + &__show-all-data { + margin-left: 12px; + } + + &__status-label { + @include font(11, 22, 600); + color: $color-gray-900; + display: block; + text-transform: uppercase; + } + + &__status-value { + @include font(13, 26, 400); + color: $color-text-tertiary; + } + + &__data { + display: flex; + flex-direction: column; + gap: 12px; + } + + &__status-toggle--toggled { + .ppcp-r-connection-status__show-all-data { + transform: rotate(180deg); + } + } + + &__status-row { + display: flex; + flex-direction: column; + * { + user-select: none; + } + strong { + @include font(14, 24, 600); + color: $color-gray-800; + margin-right: 12px; + white-space: nowrap; + } + + .ppcp-r-connection-status__status-toggle { + line-height: 0; + } + &--first { + &:hover { + cursor: pointer; + } + } + } + + @media screen and (max-width: 767px) { + flex-wrap: wrap; + &__status { + width: 100%; + } + &__status-row { + flex-wrap: wrap; + strong { + width: 100%; + } + span { + word-break: break-all; + } + } + } +} + +// Feature Refresh +.ppcp-r-feature-refresh { + display: flex; + gap: 12px; + margin-bottom: 24px; + + &__row { + display: flex; + align-items: center; + } + + &__content { + width: 100%; + + &-title { + @include font(16, 20, 700); + color: $color-black; + display: block; + margin: 0 0 4px 0; + } + + p { + @include font(12, 20, 400); + color: $color-gray-700; + margin: 0; + } + } + + button { + display: flex; + gap: 4px; + @include font(13, 20, 400); + } +} + +// Payment Methods +.ppcp-r-payment-methods { + display: flex; + flex-direction: column; + gap: 48px; +} + +// Settings Card and Block Styles +.ppcp-r-settings-card__content { + > .ppcp-r-settings-block { + &:not(:last-child) { + border-bottom: 1px solid $color-divider; + } + } +} + +.ppcp-r-settings-block { + display: flex; + flex-direction: column; + gap: 16px 0; + + &.ppcp-r-settings-block__input, + &.ppcp-r-settings-block__select { + gap: 6px 0; + } + + .ppcp-r-settings-block__header { + display: flex; + flex-direction: column; + gap: 6px; + + &:not(:last-child):not(.ppcp-r-settings-block--accordion__header) { + padding-bottom: 6px; + } + } + + .ppcp-r-settings-block__title { + @include font(11, 22, 600); + color: $color-gray-900; + display: block; + text-transform: uppercase; + + .ppcp-r-title-badge { + text-transform: none; + margin-left: 6px; + } + } + + .ppcp-r-settings-block__title-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + } + + &.ppcp-r-settings-block__feature { + .ppcp-r-settings-block__title { + @include font(13, 20, 600); + color: $color-text-text; + text-transform: none; + } + + .ppcp-r-settings-block__feature__description { + color: $color-gray-700; + @include font(13, 20, 400); + } + } + + &.ppcp-r-settings-block__toggle { + display: flex; + flex-direction: row; + + .ppcp-r-settings-block__title { + color: $color-text-text; + @include font(13, 20, 400); + text-transform: none; + } + } + + .ppcp-r-settings-block__description { + margin: 0; + @include font(13, 20, 400); + color: $color-gray-800; + + &:not(:last-child) { + padding-bottom: 1em; + } + + a { + color: $color-blueberry; + } + + strong { + color: $color-gray-800; + } + } + + .ppcp-r-settings-block__supplementary-title-label { + @include font(13, 20, 400); + color: $color-text-tertiary; + text-transform: none; + margin-left: 5px; + } + + // Types + &--toggle-content { + &.ppcp-r-settings-block--content-visible { + .ppcp-r-settings-block__toggle-content { + transform: rotate(180deg); + } + } + + .ppcp-r-settings-block__header { + user-select: none; + + &:hover { + cursor: pointer; + } + } + } + + &--sandbox-connected { + .ppcp-r-settings-block__content { + margin-top: 24px; + } + + .ppcp-r-connection-status__data { + margin-bottom: 20px; + } + } + + &--connect-sandbox { + button.components-button { + @include small-button; + } + + .ppcp-r__radio-content-additional { + .ppcp-r-vertical-text-control { + width: 100%; + } + + @include vertical-layout-event-gap(24px); + align-items: flex-start; + + input[type='text'] { + width: 100%; + } + } + } + + &--troubleshooting, + &--settings { + > .ppcp-r-settings-block__content > *:not(:last-child) { + padding-bottom: 32px; + margin-bottom: 32px; + border-bottom: 1px solid $color-gray-500; + } + } + + // Fields + input[type='text'] { + border-color: $color-gray-700; + width: 100%; + max-width: 100%; + color: $color-gray-800; + + &::placeholder { + color: $color-gray-700; + } + } + + // MultiSelect control + .ppcp-r { + &__radio-wrapper { + align-items: flex-start; + gap: 12px; + } + + &__radio-content { + display: flex; + flex-direction: column; + gap: 4px; + + label { + font-weight: 600; + } + } + + &__radio-content-additional { + padding-left: 32px; + } + + // Select control styles + &__control { + border-radius: 2px; + border-color: $color-gray-700; + min-height: auto; + padding: 0; + } + + &__input-container { + padding: 0; + margin: 0; + } + + &__value-container { + padding: 0 0 0 7px; + } + + &__indicator { + padding: 5px; + } + + &__indicator-separator { + display: none; + } + + &__value-container--has-value { + .ppcp-r__single-value { + color: $color-gray-800; + } + } + + &__placeholder, + &__single-value { + @include font(13, 20, 400); + } + + &__option { + &--is-selected { + background-color: $color-gray-200; + } + } + } +} + +// Hooks table +.ppcp-r-table { + &__hooks-url { + width: 70%; + padding-right: 20%; + text-align: left; + vertical-align: top; + } + + &__hooks-events { + vertical-align: top; + text-align: left; + width: 40%; + + span { + display: block; + } + } + + td.ppcp-r-table__hooks-url, + td.ppcp-r-table__hooks-events { + padding-top: 12px; + color: $color-gray-800; + @include font(14, 20, 400); + + span { + color: inherit; + @include font(14, 20, 400); + } + } + + th.ppcp-r-table__hooks-url, + th.ppcp-r-table__hooks-events { + @include font(14, 20, 700); + color: $color-gray-800; + border-bottom: 1px solid $color-gray-600; + padding-bottom: 4px; + } +} + +// Settings specific styles +.ppcp-r-settings-card--common-settings .ppcp-r-settings-card__content, +.ppcp-r-settings-card--expert-settings .ppcp-r-settings-card__content { + > .ppcp-r-settings-block { + &:not(:last-child) { + padding-bottom: 32px; + margin-bottom: 32px; + } + } +} + +.ppcp-r-settings-block { + &--order-intent, + &--save-payment-methods { + @include vertical-layout-event-gap(24px); + + > .ppcp-r-settings-block__content { + @include vertical-layout-event-gap(24px); + } + } +} + +.ppcp-r-settings-block--toggle-content { + .ppcp-r-settings-block__content { + margin-top: 32px; + } +} + +.ppcp-r-settings-block__button { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 50px; +} diff --git a/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss index 3399a1bc9..450251b6f 100644 --- a/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss +++ b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss @@ -20,12 +20,6 @@ margin: 0 0 24px 0; } - .ppcp-r-toggle-block__toggled-content > button{ - @include small-button; - color: $color-white; - border: none; - } - .client-id-error { color: #cc1818; margin: -16px 0 24px; @@ -78,6 +72,7 @@ border-right: 0; padding-right: 0; padding-bottom: 8px; + margin: 0px; } } } diff --git a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-overview.scss b/modules/ppcp-settings/resources/css/components/screens/overview/_tab-overview.scss deleted file mode 100644 index 5d1027043..000000000 --- a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-overview.scss +++ /dev/null @@ -1,205 +0,0 @@ -.ppcp-r-tab-overview-todo { - margin: 0 0 48px 0; -} - -.ppcp-r-todo-item { - position: relative; - display: flex; - align-items: center; - gap: 18px; - width: 100%; - - &:not(:last-child) { - border-bottom: 1px solid $color-gray-400; - padding-bottom: 24px; - } - - &:not(:first-child) { - padding-top: 24px; - } - - &__inner { - position: relative; - display: flex; - align-items: center; - gap: 18px; - } - - &__close { - margin-left: auto; - - &:hover { - cursor: pointer; - color: $color-blueberry; - } - } - - .ppcp-r__checkbox { - .components-flex { - gap: 12px; - } - label{ - @include font(13, 20, 400); - color:$color-blueberry; - } - } -} - -.ppcp-r-feature-item { - padding-top: 32px; - border-top: 1px solid $color-gray-400; - - &__title { - @include font(16, 20, 600); - color: $color-black; - display: block; - margin: 0 0 8px 0; - } - - &__description { - @include font(14, 20, 400); - color: $color-gray-800; - margin: 0 0 18px 0; - } - - &:not(:last-child) { - padding-bottom: 32px; - } - - &__buttons { - display: flex; - gap: 18px; - } - - &__notes { - display: flex; - flex-direction: column; - - span { - font-weight: 500; - } - } -} - -.ppcp-r-connection-status { - display: flex; - gap: 32px; - padding-bottom: 48px; - margin-bottom: 48px; - border-bottom: 2px solid $color-gray-500; - - &__status-status { - margin: 0 0 8px 0; - - strong { - @include font(14, 24, 700); - color: $color-black; - } - } - - &__show-all-data { - margin-left: 12px; - } - - &__status-label { - span { - @include font(12, 16, 400); - color: $color-gray-700; - } - } - - &__data { - display: flex; - flex-direction: column; - gap: 12px; - } - - &__status-toggle--toggled { - .ppcp-r-connection-status__show-all-data { - transform: rotate(180deg); - } - } - - &__status-row { - display: flex; - align-items: center; - - * { - user-select: none; - } - - strong { - @include font(14, 24, 600); - color: $color-gray-800; - margin-right: 12px; - white-space: nowrap; - } - - span:not(.ppcp-r-connection-status__status-toggle) { - @include font(14, 24, 400); - color: $color-gray-800; - } - - .ppcp-r-connection-status__status-toggle { - line-height: 0; - } - - &--first { - &:hover { - cursor: pointer; - } - } - } - - @media screen and (max-width: 767px) { - flex-wrap: wrap; - &__status { - width: 100%; - } - &__status-row { - flex-wrap: wrap; - - strong { - width: 100%; - } - - span { - word-break: break-all; - } - } - } -} - -.ppcp-r-feature-refresh { - display: flex; - gap: 12px; - margin-bottom: 24px; - - &__row { - display: flex; - align-items: center; - } - - &__content { - width: 100%; - - &-title { - @include font(16, 20, 700); - color: $color-black; - display: block; - margin: 0 0 4px 0; - } - - p { - @include font(12, 20, 400); - color: $color-gray-700; - margin: 0; - } - } - - button { - display: flex; - gap: 4px; - @include font(13, 20, 400); - } -} diff --git a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-payment-methods.scss b/modules/ppcp-settings/resources/css/components/screens/overview/_tab-payment-methods.scss deleted file mode 100644 index 556589d03..000000000 --- a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-payment-methods.scss +++ /dev/null @@ -1,5 +0,0 @@ -.ppcp-r-payment-methods{ - display: flex; - flex-direction: column; - gap:48px; -} diff --git a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-settings.scss b/modules/ppcp-settings/resources/css/components/screens/overview/_tab-settings.scss deleted file mode 100644 index 197d575ea..000000000 --- a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-settings.scss +++ /dev/null @@ -1,312 +0,0 @@ -// Global settings styles -.ppcp-r-settings { - @include vertical-layout-event-gap(48px); -} - - -.ppcp-r-settings-card__content { - > .ppcp-r-settings-block { - &:not(:last-child) { - border-bottom: 1.5px solid $color-gray-700; - } - } -} - -.ppcp-r-settings-block { - .ppcp-r-settings-block__header { - display: flex; - gap: 48px; - - &-inner { - display: flex; - flex-direction: column; - gap: 4px; - } - } - - &__action { - margin-left: auto; - } - - &--primary { - > .ppcp-r-settings-block__header { - .ppcp-r-settings-block__title { - @include font(16, 20, 700); - color: $color-black; - } - } - } - - &--secondary { - > .ppcp-r-settings-block__header { - .ppcp-r-settings-block__title { - @include font(16, 20, 600); - color: $color-gray-800; - } - } - } - - &--tertiary { - padding-bottom: 0; - margin-bottom: 24px; - - > .ppcp-r-settings-block__header { - align-items: center; - - .ppcp-r-settings-block__title { - color: $color-gray-800; - @include font(14, 20, 400); - } - } - } - - .ppcp-r-settings-block__description { - margin: 0; - @include font(14, 20, 400); - color: $color-gray-800; - - a { - color: $color-blueberry; - } - - strong { - color: $color-gray-800; - } - } - - // Types - &--toggle-content { - &.ppcp-r-settings-block--content-visible { - .ppcp-r-settings-block__toggle-content { - transform: rotate(180deg); - } - } - - .ppcp-r-settings-block__header { - user-select: none; - - &:hover { - cursor: pointer; - } - } - } - - &--sandbox-connected { - .ppcp-r-settings-block__content { - margin-top: 24px; - } - - button.is-secondary { - @include small-button; - } - - .ppcp-r-connection-status__data { - margin-bottom: 20px; - } - } - - &--expert-rdb{ - @include vertical-layout-event-gap(24px); - } - &--connect-sandbox { - button.components-button { - @include small-button; - } - - .ppcp-r__radio-content-additional { - .ppcp-r-vertical-text-control { - width: 100%; - } - - @include vertical-layout-event-gap(24px); - align-items: flex-start; - - input[type='text'] { - width: 100%; - } - } - } - - &--troubleshooting { - > .ppcp-r-settings-block__content > *:not(:last-child) { - padding-bottom: 32px; - margin-bottom: 32px; - border-bottom: 1px solid $color-gray-500; - } - } - - &--settings{ - > .ppcp-r-settings-block__content > *:not(:last-child){ - padding-bottom: 32px; - margin-bottom: 32px; - border-bottom: 1px solid $color-gray-500; - } - } - - // Fields - input[type='text'] { - border-color: $color-gray-700; - width: 282px; - max-width: 100%; - color: $color-gray-800; - } - - input[type='text'] { - &::placeholder { - color: $color-gray-700; - } - } - - .ppcp-r { - &__radio-wrapper { - align-items: flex-start; - gap: 12px; - } - - &__radio-content { - display: flex; - flex-direction: column; - gap: 4px; - - label { - font-weight: 600; - } - } - - &__radio-content-additional { - padding-left: 32px; - } - } - - // MultiSelect control - .ppcp-r { - &__control { - border-radius: 2px; - border-color: $color-gray-700; - width: 282px; - min-height: auto; - padding: 0; - } - - &__input-container { - padding: 0; - margin: 0; - } - - &__value-container { - padding: 0 0 0 7px; - } - - &__indicator { - padding: 5px; - } - - &__indicator-separator { - display: none; - } - - &__value-container--has-value { - .ppcp-r__single-value { - color: $color-gray-800; - } - } - - &__placeholde, &__single-value { - @include font(13, 20, 400); - } - - &__option { - &--is-selected { - background-color: $color-gray-200; - } - } - } -} - -// Special settings styles - -// Hooks table -.ppcp-r-table { - &__hooks-url { - width: 70%; - padding-right: 20%; - text-align: left; - vertical-align: top; - } - - &__hooks-events { - vertical-align: top; - text-align: left; - width: 40%; - - span { - display: block; - } - } - - td.ppcp-r-table__hooks-url, td.ppcp-r-table__hooks-events { - padding-top: 12px; - color: $color-gray-800; - @include font(14, 20, 400); - - span { - color: inherit; - @include font(14, 20, 400); - } - } - - th.ppcp-r-table__hooks-url, th.ppcp-r-table__hooks-events { - @include font(14, 20, 700); - color: $color-gray-800; - border-bottom: 1px solid $color-gray-600; - padding-bottom: 4px; - } -} - -// Common settings have 48px margin&padding bottom between blocks -.ppcp-r-settings-card--common-settings .ppcp-r-settings-card__content { - > .ppcp-r-settings-block { - &:not(:last-child) { - padding-bottom: 48px; - margin-bottom: 48px; - } - } -} - -// Expert settings have 32px margin&padding bottom between blocks -.ppcp-r-settings-card--expert-settings .ppcp-r-settings-card__content { - > .ppcp-r-settings-block { - &:not(:last-child) { - padding-bottom: 32px; - margin-bottom: 32px; - } - } -} - - -// Order intent block has 32px gap and no lines in between -// Save payment methods block has 32px gap and no lines in between -.ppcp-r-settings-block { - &--order-intent, &--save-payment-methods { - @include vertical-layout-event-gap(32px); - - > .ppcp-r-settings-block__content { - @include vertical-layout-event-gap(32px); - } - } -} - - -// Most primary settings block in the expert settings have 32px space after description -.ppcp-r-settings-block--toggle-content { - .ppcp-r-settings-block__content { - margin-top: 32px; - } -} - -// Common settings have actions aligned top with the text, Expert settings have actions alligned middle with the text -.ppcp-r-settings-card--expert-settings { - .ppcp-r-settings-block__header { - align-items: center; - } -} diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_block-accordion.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_block-accordion.scss new file mode 100644 index 000000000..c77a3eb91 --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_block-accordion.scss @@ -0,0 +1,38 @@ +.ppcp-r-settings-block__accordion { + > .ppcp-r-accordion { + width: 100%; + + .ppcp-r-accordion__toggler { + width: 100%; + margin: 0; + text-align: unset; + } + } + + &.ppcp-r-settings-block { + gap: 0; + + .ppcp-r-settings-block__title { + @include font(13, 20, 600); + color: $color-text-text; + text-transform: none; + } + + .ppcp-r-settings-block--accordion__title { + @include font(14, 20, 600); + } + + .ppcp-r-settings-block--accordion__description { + color: $color-gray-700; + @include font(13, 20, 400); + } + + .ppcp-r-settings-block:not(:last-child) { + &:not(.ppcp-r__radio-content-additional .ppcp-r-settings-block) { + padding-bottom: 32px; + margin-bottom: 32px; + border-bottom: 1px solid $color-divider; + } + } + } +} diff --git a/modules/ppcp-settings/resources/css/style.scss b/modules/ppcp-settings/resources/css/style.scss index a1f5b390b..56fe55a62 100644 --- a/modules/ppcp-settings/resources/css/style.scss +++ b/modules/ppcp-settings/resources/css/style.scss @@ -3,10 +3,11 @@ #ppcp-settings-container { @import './global'; - @import './components/reusable-components/onboarding-header'; + @import './components/reusable-components/busy-state'; @import './components/reusable-components/button'; - @import './components/reusable-components/settings-toggle-block'; @import './components/reusable-components/separator'; + @import './components/reusable-components/onboarding-header'; + @import './components/reusable-components/settings-toggle-block'; @import './components/reusable-components/payment-method-icons'; @import "./components/reusable-components/payment-method-item"; @import './components/reusable-components/settings-wrapper'; @@ -21,12 +22,9 @@ @import './components/reusable-components/welcome-docs'; @import './components/screens/onboarding'; @import './components/screens/settings'; - @import './components/screens/overview/tab-overview'; - @import './components/screens/overview/tab-payment-methods'; - @import './components/screens/overview/tab-settings'; @import './components/screens/overview/tab-styling'; + @import './components/app'; } @import './components/reusable-components/payment-method-modal'; -@import './components/screens/onboarding-global'; -@import './components/screens/settings-global'; +@import './components/screens/fullscreen'; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js index 23f01a09c..f5b071945 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js @@ -1,65 +1,72 @@ -import { useEffect } from '@wordpress/element'; import { Icon } from '@wordpress/components'; import { chevronDown, chevronUp } from '@wordpress/icons'; -import { useState } from 'react'; +import classNames from 'classnames'; + +import { useAccordionState } from '../../hooks/useAccordionState'; + +// Provide defaults for all layout components so the generic version just works. +const DefaultHeader = ( { children, className = '' } ) => ( +
+ { children } +
+); +const DefaultTitleWrapper = ( { children } ) => ( +
{ children }
+); +const DefaultTitle = ( { children } ) => ( + { children } +); +const DefaultAction = ( { children } ) => ( + { children } +); +const DefaultDescription = ( { children } ) => ( +
{ children }
+); const Accordion = ( { title, + id = '', initiallyOpen = null, + description = '', + children = null, className = '', - id = '', - children, -} ) => { - const determineInitialState = () => { - if ( id && initiallyOpen === null ) { - return window.location.hash === `#${ id }`; - } - return !! initiallyOpen; - }; - - const [ isOpen, setIsOpen ] = useState( determineInitialState ); - useEffect( () => { - const handleHashChange = () => { - if ( id && window.location.hash === `#${ id }` ) { - setIsOpen( true ); - } - }; - - window.addEventListener( 'hashchange', handleHashChange ); - - return () => { - window.removeEventListener( 'hashchange', handleHashChange ); - }; - }, [ id ] ); - - const toggleOpen = ( ev ) => { - setIsOpen( ! isOpen ); - ev?.preventDefault(); - return false; - }; + // Layout components can be overridden by the caller + Header = DefaultHeader, + TitleWrapper = DefaultTitleWrapper, + Title = DefaultTitle, + Action = DefaultAction, + Description = DefaultDescription, +} ) => { + const { isOpen, toggleOpen } = useAccordionState( { id, initiallyOpen } ); + const wrapperClasses = classNames( 'ppcp-r-accordion', className, { + 'ppcp--is-open': isOpen, + } ); - const wrapperClasses = [ 'ppcp-r-accordion' ]; - if ( className ) { - wrapperClasses.push( className ); - } - if ( isOpen ) { - wrapperClasses.push( 'ppcp--is-open' ); - } + const icon = isOpen ? chevronUp : chevronDown; return ( -
+
- { isOpen && ( -
{ children }
+ { isOpen && children && ( +
{ children }
) }
); diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js new file mode 100644 index 000000000..959b71bfe --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js @@ -0,0 +1,68 @@ +import { + Children, + isValidElement, + cloneElement, + useMemo, + createContext, + useContext, +} from '@wordpress/element'; +import classNames from 'classnames'; + +import { CommonHooks } from '../../data'; +import SpinnerOverlay from './SpinnerOverlay'; + +// Create context to track the busy state across nested wrappers +const BusyContext = createContext( false ); + +/** + * Wraps interactive child elements and modifies their behavior based on the global `isBusy` state. + * Allows custom processing of child props via the `onBusy` callback. + * + * @param {Object} props - Component properties. + * @param {Children} props.children - Child components to wrap. + * @param {boolean} props.enabled - Enables or disables the busy-state logic. + * @param {boolean} props.busySpinner - Allows disabling the spinner in busy-state. + * @param {string} props.className - Additional class names for the wrapper. + * @param {Function} props.onBusy - Callback to process child props when busy. + */ +const BusyStateWrapper = ( { + children, + enabled = true, + busySpinner = true, + className = '', + onBusy = () => ( { disabled: true } ), +} ) => { + const { isBusy } = CommonHooks.useBusyState(); + const hasBusyParent = useContext( BusyContext ); + + const isBusyComponent = isBusy && enabled; + const showSpinner = busySpinner && isBusyComponent && ! hasBusyParent; + + const wrapperClassName = classNames( 'ppcp-r-busy-wrapper', className, { + 'ppcp--is-loading': isBusyComponent, + } ); + + const memoizedChildren = useMemo( + () => + Children.map( children, ( child ) => + isValidElement( child ) + ? cloneElement( + child, + isBusyComponent ? onBusy( child.props ) : {} + ) + : child + ), + [ children, isBusyComponent, onBusy ] + ); + + return ( + +
+ { showSpinner && } + { memoizedChildren } +
+
+ ); +}; + +export default BusyStateWrapper; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js index bfa45013e..5fbfdb1f1 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/ConnectionInfo.js @@ -1,5 +1,4 @@ import { __ } from '@wordpress/i18n'; -import data from '../../utils/data'; import { useState } from '@wordpress/element'; const ConnectionInfo = ( { connectionStatusDataDefault } ) => { @@ -7,13 +6,6 @@ const ConnectionInfo = ( { connectionStatusDataDefault } ) => { ...connectionStatusDataDefault, } ); - const showAllData = () => { - setConnectionData( { - ...connectionData, - showAllData: ! connectionData.showAllData, - } ); - }; - const toggleStatusClassName = [ 'ppcp-r-connection-status__status-toggle' ]; if ( connectionData.showAllData ) { @@ -24,43 +16,30 @@ const ConnectionInfo = ( { connectionStatusDataDefault } ) => { return (
-
showAllData() } - > - - { __( 'Email address:', 'woocommerce-paypal-payments' ) } - - { connectionData.email } - - { data().getImage( - 'icon-arrow-down.svg', - 'ppcp-r-connection-status__show-all-data' - ) } +
+ + { __( 'Merchant ID', 'woocommerce-paypal-payments' ) } + + + { connectionData.merchantId } + +
+
+ + { __( 'Email address', 'woocommerce-paypal-payments' ) } + + + { connectionData.email } + +
+
+ + { __( 'Client ID', 'woocommerce-paypal-payments' ) } + + + { connectionData.clientId }
- { connectionData.showAllData && ( - <> -
- - { __( - 'Merchant ID:', - 'woocommerce-paypal-payments' - ) } - - { connectionData.merchantId } -
-
- - { __( - 'Client ID:', - 'woocommerce-paypal-payments' - ) } - - { connectionData.clientId } -
- - ) }
); }; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons.js new file mode 100644 index 000000000..3344c3ceb --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons.js @@ -0,0 +1 @@ +export { default as openSignup } from './Icons/open-signup'; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons/open-signup.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons/open-signup.js new file mode 100644 index 000000000..83c792f22 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Icons/open-signup.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const openSignup = ( + + + +); + +export default openSignup; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/AcdcOptionalPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/AcdcOptionalPaymentMethods.js index f316a9a90..2abcc37a9 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/AcdcOptionalPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/AcdcOptionalPaymentMethods.js @@ -1,4 +1,4 @@ -import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox'; +import BadgeBox from '../BadgeBox'; import { __, sprintf } from '@wordpress/i18n'; import Separator from '../Separator'; import generatePriceText from '../../../utils/badgeBoxUtils'; @@ -10,7 +10,7 @@ const AcdcOptionalPaymentMethods = ( { storeCountry, storeCurrency, } ) => { - if ( isFastlane && isPayLater && storeCountry === 'us' ) { + if ( isFastlane && isPayLater && storeCountry === 'US' ) { return (
{ -
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/PaymentMethodItem.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/PaymentMethodItem.js deleted file mode 100644 index dbe9c6971..000000000 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/PaymentMethodItem.js +++ /dev/null @@ -1,65 +0,0 @@ -import { Button } from '@wordpress/components'; -import PaymentMethodIcon from './PaymentMethodIcon'; -import { PayPalCheckbox } from './Fields'; -import { useState } from '@wordpress/element'; -import { ToggleControl } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import data from '../../utils/data'; - -const PaymentMethodItem = ( props ) => { - const [ paymentMethodState, setPaymentMethodState ] = useState(); - const [ modalIsVisible, setModalIsVisible ] = useState( false ); - let Modal = null; - if ( props?.modal ) { - Modal = props.modal; - } - const handleCheckboxState = ( checked ) => { - if ( checked ) { - setPaymentMethodState( props.payment_method_id ); - } else { - setPaymentMethodState( null ); - } - }; - return ( - <> -
-
-
- - - { props.title } - -
-
-

{ props.description }

-
-
- - handleCheckboxState( newValue ) - } - /> -
setModalIsVisible( true ) } - > - { Modal && data().getImage( 'icon-settings.svg' ) } -
-
-
-
- { Modal && modalIsVisible && ( - - ) } - - ); -}; - -export default PaymentMethodItem; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlock.js deleted file mode 100644 index d23860b38..000000000 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlock.js +++ /dev/null @@ -1,146 +0,0 @@ -import { Button, ToggleControl, TextControl } from '@wordpress/components'; -import data from '../../utils/data'; -import { useState } from '@wordpress/element'; -import Select, { components } from 'react-select'; - -export const SETTINGS_BLOCK_TYPE_EMPTY = 'empty'; -export const SETTINGS_BLOCK_TYPE_TOGGLE = 'toggle'; -export const SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT = 'toggle-content'; -export const SETTINGS_BLOCK_TYPE_INPUT = 'input'; -export const SETTINGS_BLOCK_TYPE_BUTTON = 'button'; -export const SETTINGS_BLOCK_TYPE_SELECT = 'select'; - -export const SETTINGS_BLOCK_STYLING_TYPE_PRIMARY = 'primary'; -export const SETTINGS_BLOCK_STYLING_TYPE_SECONDARY = 'secondary'; -export const SETTINGS_BLOCK_STYLING_TYPE_TERTIARY = 'tertiary'; - -const SettingsBlock = ( { - className, - title, - description, - children, - style, - actionProps, - tag, -} ) => { - const [ toggleContentVisible, setToggleContentVisible ] = useState( - actionProps?.type !== SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT - ); - - const toggleContent = () => { - if ( actionProps?.type !== SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT ) { - return; - } - setToggleContentVisible( ! toggleContentVisible ); - }; - - const blockClassName = [ 'ppcp-r-settings-block' ]; - - blockClassName.push( 'ppcp-r-settings-block--' + style ); - blockClassName.push( 'ppcp-r-settings-block--' + actionProps?.type ); - - if ( className ) { - blockClassName.push( className ); - } - - if ( - toggleContentVisible && - actionProps?.type === SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT - ) { - blockClassName.push( 'ppcp-r-settings-block--content-visible' ); - } - - return ( -
-
toggleContent() } - > -
- - { title } - { tag && tag } - -

-

- { actionProps?.type !== SETTINGS_BLOCK_TYPE_EMPTY && ( -
- { actionProps?.type === SETTINGS_BLOCK_TYPE_TOGGLE && ( - - actionProps?.callback( - actionProps?.key, - newValue - ) - } - /> - ) } - { actionProps?.type === SETTINGS_BLOCK_TYPE_INPUT && ( - <> - - actionProps?.callback( - actionProps?.key, - newValue - ) - } - /> - - ) } - { actionProps?.type === SETTINGS_BLOCK_TYPE_BUTTON && ( - - ) } - { actionProps?.type === - SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT && ( -
- { data().getImage( 'icon-arrow-down.svg' ) } -
- ) } - { actionProps?.type === SETTINGS_BLOCK_TYPE_SELECT && ( - + + ), + description: ( { description } ) => ( + { description } + ), +}; + +const SelectSettingsBlock = ( { + title, + description, + order = DEFAULT_ELEMENT_ORDER, + ...props +} ) => ( + ( + <> + { order.map( ( elementKey ) => { + const RenderElement = ELEMENT_RENDERERS[ elementKey ]; + return RenderElement ? ( + + ) : null; + } ) } + + ), + ] } + /> +); + +export default SelectSettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js new file mode 100644 index 000000000..5e4985104 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlock.js @@ -0,0 +1,15 @@ +const SettingsBlock = ( { className, components = [] } ) => { + const blockClassName = [ 'ppcp-r-settings-block', className ].filter( + Boolean + ); + + return ( +
+ { components.map( ( Component, index ) => ( + + ) ) } +
+ ); +}; + +export default SettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js new file mode 100644 index 000000000..296c2c2ad --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js @@ -0,0 +1,42 @@ +// Block Elements +export const Title = ( { children, className = '' } ) => ( + + { children } + +); +export const TitleWrapper = ( { children } ) => ( + { children } +); + +export const SupplementaryLabel = ( { children } ) => ( + + { children } + +); + +export const Description = ( { children, className = '' } ) => ( + + { children } + +); + +export const Action = ( { children } ) => ( +
{ children }
+); + +export const Header = ( { children, className = '' } ) => ( +
+ { children } +
+); + +// Card Elements +export const Content = ( { children } ) => ( +
{ children }
+); + +export const ContentWrapper = ( { children } ) => ( +
{ children }
+); diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js new file mode 100644 index 000000000..4f9b01644 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/TodoSettingsBlock.js @@ -0,0 +1,69 @@ +import { PayPalCheckbox, handleCheckboxState } from '../Fields'; +import data from '../../../utils/data'; + +const TodoSettingsBlock = ( { + todos, + setTodos, + todosData, + setTodosData, + className = '', +} ) => { + if ( todosData.length === 0 ) { + return null; + } + + return ( +
+ { todosData.map( ( todo ) => ( + + ) ) } +
+ ); +}; + +const TodoItem = ( props ) => { + return ( +
+
+ +
+ { props.description } +
+
+
+ removeTodo( + props.value, + props.todosData, + props.changeTodos + ) + } + > + { data().getImage( 'icon-close.svg' ) } +
+
+ ); +}; + +const removeTodo = ( todoValue, todosData, changeTodos ) => { + changeTodos( todosData.filter( ( todo ) => todo.value !== todoValue ) ); +}; + +export default TodoSettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ToggleSettingsBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ToggleSettingsBlock.js new file mode 100644 index 000000000..3e0c0eac6 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/ToggleSettingsBlock.js @@ -0,0 +1,37 @@ +import { ToggleControl } from '@wordpress/components'; +import SettingsBlock from './SettingsBlock'; +import { Header, Title, Action, Description } from './SettingsBlockElements'; + +const ToggleSettingsBlock = ( { title, description, ...props } ) => ( + ( + + + props.actionProps?.callback( + props.actionProps?.key, + newValue + ) + } + /> + + ), + () => ( +
+ { title && { title } } + { description && ( + { description } + ) } +
+ ), + ] } + /> +); + +export default ToggleSettingsBlock; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/index.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/index.js new file mode 100644 index 000000000..80a5db448 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/index.js @@ -0,0 +1,20 @@ +export { default as SettingsBlock } from './SettingsBlock'; +export { default as ButtonSettingsBlock } from './ButtonSettingsBlock'; +export { default as InputSettingsBlock } from './InputSettingsBlock'; +export { default as SelectSettingsBlock } from './SelectSettingsBlock'; +export { default as AccordionSettingsBlock } from './AccordionSettingsBlock'; +export { default as ToggleSettingsBlock } from './ToggleSettingsBlock'; +export { default as RadioSettingsBlock } from './RadioSettingsBlock'; +export { default as PaymentMethodsBlock } from './PaymentMethodsBlock'; +export { default as PaymentMethodItemBlock } from './PaymentMethodItemBlock'; + +export { + Title, + TitleWrapper, + SupplementaryLabel, + Description, + Action, + Content, + ContentWrapper, + Header, +} from './SettingsBlockElements'; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js index 11693d172..aeb0e3561 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js @@ -1,26 +1,50 @@ -import data from '../../utils/data'; +import { Content, ContentWrapper } from './SettingsBlocks'; -const SettingsCard = ( props ) => { - let className = 'ppcp-r-settings-card'; +const SettingsCard = ( { + className: extraClassName, + title, + description, + children, + contentItems, + contentContainer = true, +} ) => { + const className = [ 'ppcp-r-settings-card', extraClassName ] + .filter( Boolean ) + .join( ' ' ); + + const renderContent = () => { + // If contentItems array is provided, wrap each item in Content component + if ( contentItems ) { + return ( + + { contentItems.map( ( item, index ) => ( + { item } + ) ) } + + ); + } + + // Otherwise handle regular children with contentContainer prop + if ( contentContainer ) { + return { children }; + } + + return children; + }; - if ( props?.className ) { - className += ' ' + props.className; - } return (
- { props.title } + { title }

- { props.description } + { description }

-
- { props.children } -
+ { renderContent() }
); }; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsToggleBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsToggleBlock.js index d8dda1cfb..4a7cf1a20 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsToggleBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsToggleBlock.js @@ -1,23 +1,17 @@ import { ToggleControl } from '@wordpress/components'; import { useRef } from '@wordpress/element'; -import SpinnerOverlay from './SpinnerOverlay'; - const SettingsToggleBlock = ( { isToggled, setToggled, - isLoading = false, + disabled = false, ...props } ) => { const toggleRef = useRef( null ); const blockClasses = [ 'ppcp-r-toggle-block' ]; - if ( isLoading ) { - blockClasses.push( 'ppcp--is-loading' ); - } - const handleLabelClick = () => { - if ( ! toggleRef.current || isLoading ) { + if ( ! toggleRef.current || disabled ) { return; } @@ -52,13 +46,12 @@ const SettingsToggleBlock = ( { ref={ toggleRef } checked={ isToggled } onChange={ ( newState ) => setToggled( newState ) } - disabled={ isLoading } + disabled={ disabled } />
{ props.children && isToggled && (
- { isLoading && } { props.children }
) } diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js index dec732a3e..b4165b5ba 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js @@ -1,8 +1,13 @@ import { Spinner } from '@wordpress/components'; -const SpinnerOverlay = () => { +const SpinnerOverlay = ( { message = '' } ) => { return (
+ { message && ( + + { message } + + ) }
); diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/TitleBadge.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/TitleBadge.js index dc9cdad71..9b493e735 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/TitleBadge.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/TitleBadge.js @@ -1,11 +1,13 @@ const TitleBadge = ( { text, type } ) => { const className = 'ppcp-r-title-badge ' + `ppcp-r-title-badge--${ type }`; - return ; + return ( + + ); }; export const TITLE_BADGE_POSITIVE = 'positive'; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js index 1c167d756..9779e789e 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js @@ -12,7 +12,7 @@ const AcdcFlow = ( { storeCountry, storeCurrency, } ) => { - if ( isFastlane && isPayLater && storeCountry === 'us' ) { + if ( isFastlane && isPayLater && storeCountry === 'US' ) { return (
@@ -66,7 +66,7 @@ const AcdcFlow = ( { description={ sprintf( // translators: %s: Link to PayPal business fees guide __( - 'Offer installment payment options and get paid upfront - at no extra cost to you. Learn more', + 'Offer installment payment options and get paid upfront. Learn more', 'woocommerce-paypal-payments' ), 'https://www.paypal.com/us/business/paypal-business-fees' @@ -123,7 +123,7 @@ const AcdcFlow = ( { ); } - if ( isPayLater && storeCountry === 'uk' ) { + if ( isPayLater && storeCountry === 'UK' ) { return (
@@ -256,7 +256,7 @@ const AcdcFlow = ( { description={ sprintf( // translators: %s: Link to PayPal REST application guide __( - 'Offer installment payment options and get paid upfront - at no extra cost to you. Learn more', + 'Offer installment payment options and get paid upfront. Learn more', 'woocommerce-paypal-payments' ), 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js index 6c984cfc1..99abfc4f2 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js @@ -6,7 +6,7 @@ import { countryPriceInfo } from '../../../utils/countryPriceInfo'; import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods'; const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => { - if ( isPayLater && storeCountry === 'us' ) { + if ( isPayLater && storeCountry === 'US' ) { return (
@@ -60,7 +60,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => { description={ sprintf( // translators: %s: Link to PayPal REST application guide __( - 'Offer installment payment options and get paid upfront - at no extra cost to you. Learn more', + 'Offer installment payment options and get paid upfront. Learn more', 'woocommerce-paypal-payments' ), 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' @@ -158,7 +158,7 @@ const BcdcFlow = ( { isPayLater, storeCountry, storeCurrency } ) => { description={ sprintf( // translators: %s: Link to PayPal REST application guide __( - 'Offer installment payment options and get paid upfront - at no extra cost to you. Learn more', + 'Offer installment payment options and get paid upfront. Learn more', 'woocommerce-paypal-payments' ), 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js index b3b60fee1..eabb5b3db 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js @@ -1,7 +1,8 @@ -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import AcdcFlow from './AcdcFlow'; import BcdcFlow from './BcdcFlow'; -import { Button } from '@wordpress/components'; +import { countryPriceInfo } from '../../../utils/countryPriceInfo'; +import { pricesBasedDescription } from './pricesBasedDescription'; const WelcomeDocs = ( { useAcdc, @@ -10,15 +11,6 @@ const WelcomeDocs = ( { storeCountry, storeCurrency, } ) => { - const pricesBasedDescription = sprintf( - // translators: %s: Link to PayPal REST application guide - __( - '1Prices based on domestic transactions as of October 25th, 2024. Click here for full pricing details.', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - ); - return (

@@ -41,10 +33,14 @@ const WelcomeDocs = ( { storeCurrency={ storeCurrency } /> ) } -

+ { storeCountry in countryPriceInfo && ( +

+ ) }

); }; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/pricesBasedDescription.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/pricesBasedDescription.js new file mode 100644 index 000000000..c4d3eb983 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/pricesBasedDescription.js @@ -0,0 +1,10 @@ +import { __, sprintf } from '@wordpress/i18n'; + +export const pricesBasedDescription = sprintf( + // translators: %s: Link to PayPal REST application guide + __( + '1Prices based on domestic transactions as of October 25th, 2024. Click here for full pricing details.', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' +); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index e7891955b..6aabd15fd 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -1,96 +1,118 @@ import { __, sprintf } from '@wordpress/i18n'; import { Button, TextControl } from '@wordpress/components'; -import { useRef, useMemo } from '@wordpress/element'; -import { useDispatch } from '@wordpress/data'; -import { store as noticesStore } from '@wordpress/notices'; +import { + useRef, + useState, + useEffect, + useMemo, + useCallback, +} from '@wordpress/element'; + +import classNames from 'classnames'; import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock'; import Separator from '../../../ReusableComponents/Separator'; import DataStoreControl from '../../../ReusableComponents/DataStoreControl'; import { CommonHooks } from '../../../../data'; -import { openPopup } from '../../../../utils/window'; +import { + useSandboxConnection, + useManualConnection, +} from '../../../../hooks/useHandleConnections'; + +import ConnectionButton from './ConnectionButton'; +import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; + +const FORM_ERRORS = { + noClientId: __( + 'Please enter your Client ID', + 'woocommerce-paypal-payments' + ), + noClientSecret: __( + 'Please enter your Secret Key', + 'woocommerce-paypal-payments' + ), + invalidClientId: __( + 'Please enter a valid Client ID', + 'woocommerce-paypal-payments' + ), +}; + +const AdvancedOptionsForm = () => { + const [ clientValid, setClientValid ] = useState( false ); + const [ secretValid, setSecretValid ] = useState( false ); -const AdvancedOptionsForm = ( { setCompleted } ) => { const { isBusy } = CommonHooks.useBusyState(); - const { isSandboxMode, setSandboxMode, connectViaSandbox } = - CommonHooks.useSandbox(); + const { isSandboxMode, setSandboxMode } = useSandboxConnection(); const { + handleConnectViaIdAndSecret, isManualConnectionMode, setManualConnectionMode, clientId, setClientId, clientSecret, setClientSecret, - connectViaIdAndSecret, - } = CommonHooks.useManualConnection(); + } = useManualConnection(); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); const refClientId = useRef( null ); const refClientSecret = useRef( null ); - const isValidClientId = useMemo( () => { - return /^A[\w-]{79}$/.test( clientId ); - }, [ clientId ] ); - - const isFormValid = useMemo( () => { - return isValidClientId && clientId && clientSecret; - }, [ isValidClientId, clientId, clientSecret ] ); - - const handleServerError = ( res, genericMessage ) => { - console.error( 'Connection error', res ); - createErrorNotice( res?.message ?? genericMessage ); - }; - - const handleServerSuccess = () => { - createSuccessNotice( - __( 'Connected to PayPal', 'woocommerce-paypal-payments' ) - ); - setCompleted( true ); - }; - - const handleSandboxConnect = async () => { - const res = await connectViaSandbox(); - - if ( ! res.success || ! res.data ) { - handleServerError( - res, - __( - 'Could not generate a Sandbox login link.', - 'woocommerce-paypal-payments' - ) - ); - return; + const validateManualConnectionForm = useCallback( () => { + const checks = [ + { + ref: refClientId, + valid: () => clientId, + errorMessage: FORM_ERRORS.noClientId, + }, + { + ref: refClientId, + valid: () => clientValid, + errorMessage: FORM_ERRORS.invalidClientId, + }, + { + ref: refClientSecret, + valid: () => clientSecret && secretValid, + errorMessage: FORM_ERRORS.noClientSecret, + }, + ]; + + for ( const { ref, valid, errorMessage } of checks ) { + if ( valid() ) { + continue; + } + + ref?.current?.focus(); + throw new Error( errorMessage ); } + }, [ clientId, clientSecret, clientValid, secretValid ] ); + + const handleManualConnect = useCallback( + () => + handleConnectViaIdAndSecret( { + validation: validateManualConnectionForm, + } ), + [ handleConnectViaIdAndSecret, validateManualConnectionForm ] + ); - const connectionUrl = res.data; - const popup = openPopup( connectionUrl ); + useEffect( () => { + setClientValid( ! clientId || /^A[\w-]{79}$/.test( clientId ) ); + setSecretValid( clientSecret && clientSecret.length > 0 ); + }, [ clientId, clientSecret ] ); + + const clientIdLabel = useMemo( + () => + isSandboxMode + ? __( 'Sandbox Client ID', 'woocommerce-paypal-payments' ) + : __( 'Live Client ID', 'woocommerce-paypal-payments' ), + [ isSandboxMode ] + ); - if ( ! popup ) { - createErrorNotice( - __( - 'Popup blocked. Please allow popups for this site to connect to PayPal.', - 'woocommerce-paypal-payments' - ) - ); - } - }; - - const handleManualConnect = async () => { - const res = await connectViaIdAndSecret(); - - if ( res.success ) { - handleServerSuccess(); - } else { - handleServerError( - res, - __( - 'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.', - 'woocommerce-paypal-payments' - ) - ); - } - }; + const secretKeyLabel = useMemo( + () => + isSandboxMode + ? __( 'Sandbox Secret Key', 'woocommerce-paypal-payments' ) + : __( 'Live Secret Key', 'woocommerce-paypal-payments' ), + [ isSandboxMode ] + ); const advancedUsersDescription = sprintf( // translators: %s: Link to PayPal REST application guide @@ -103,88 +125,84 @@ const AdvancedOptionsForm = ( { setCompleted } ) => { return ( <> - - - + + + + + - ( { + disabled: true, + label: props.label + ' ...', + } ) } > - - { clientId && ! isValidClientId && ( -

+ + + { clientValid || ( +

+ { FORM_ERRORS.invalidClientId } +

+ ) } + + -
+ + + ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js new file mode 100644 index 000000000..ad6a7dcef --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js @@ -0,0 +1,49 @@ +import { Button } from '@wordpress/components'; + +import classNames from 'classnames'; + +import { CommonHooks } from '../../../../data'; +import { openSignup } from '../../../ReusableComponents/Icons'; +import { + useProductionConnection, + useSandboxConnection, +} from '../../../../hooks/useHandleConnections'; +import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; + +const ConnectionButton = ( { + title, + isSandbox = false, + variant = 'primary', + showIcon = true, + className = '', +} ) => { + const { handleSandboxConnect } = useSandboxConnection(); + const { handleProductionConnect } = useProductionConnection(); + const buttonClassName = classNames( 'ppcp-r-connection-button', className, { + 'sandbox-mode': isSandbox, + 'live-mode': ! isSandbox, + } ); + + const handleConnectClick = async () => { + if ( isSandbox ) { + await handleSandboxConnect(); + } else { + await handleProductionConnect(); + } + }; + + return ( + + + + ); +}; + +export default ConnectionButton; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js index 5a1da25cb..3c12e1206 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js @@ -1,130 +1,81 @@ -import { Button } from '@wordpress/components'; +import { Button, Icon } from '@wordpress/components'; +import { chevronLeft } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; -import { OnboardingHooks } from '../../../../data'; -import data from '../../../../utils/data'; - -const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => { - const isLastStep = () => currentStep + 1 === stepperOrder.length; - const isFistStep = () => currentStep === 0; - const navigateBy = ( stepDirection ) => { - let newStep = currentStep + stepDirection; - - if ( isNaN( newStep ) || newStep < 0 ) { - console.warn( 'Invalid next step:', newStep ); - newStep = 0; - } - - if ( newStep >= stepperOrder.length ) { - setCompleted( true ); - } else { - setStep( newStep ); - } - }; +import classNames from 'classnames'; - const { products } = OnboardingHooks.useProducts(); - const { isCasualSeller } = OnboardingHooks.useBusiness(); +import { OnboardingHooks } from '../../../../data'; +import useIsScrolled from '../../../../hooks/useIsScrolled'; +import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; - let navigationTitle = ''; - let disabled = false; +const Navigation = ( { stepDetails, onNext, onPrev, onExit } ) => { + const { title, isFirst, percentage, showNext, canProceed } = stepDetails; + const { isScrolled } = useIsScrolled(); - switch ( currentStep ) { - case 1: - navigationTitle = __( - 'Set up store type', - 'woocommerce-paypal-payments' - ); - disabled = isCasualSeller === null; - break; - case 2: - navigationTitle = __( - 'Select product types', - 'woocommerce-paypal-payments' - ); - disabled = products.length < 1; - break; - case 3: - navigationTitle = __( - 'Choose checkout options', - 'woocommerce-paypal-payments' - ); - case 4: - navigationTitle = __( - 'Connect your PayPal account', - 'woocommerce-paypal-payments' - ); - break; - default: - navigationTitle = __( - 'PayPal Payments', - 'woocommerce-paypal-payments' - ); - } + const state = OnboardingHooks.useNavigationState(); + const isDisabled = ! canProceed( state ); + const className = classNames( 'ppcp-r-navigation-container', { + 'is-scrolled': isScrolled, + } ); return ( -
+
-
- { data().getImage( 'icon-arrow-left.svg' ) } - { ! isFistStep() ? ( - - ) : ( - - { navigationTitle } - - ) } -
- { ! isFistStep() && ( -
- - { __( - 'Save and exit', - 'woocommerce-paypal-payments' - ) } - - { ! isLastStep() && ( - - ) } -
- ) } -
+ + + + { ! isFirst && + NextButton( { showNext, isDisabled, onNext, onExit } ) } +
); }; +const NextButton = ( { showNext, isDisabled, onNext, onExit } ) => { + return ( + + + { showNext && ( + + ) } + + ); +}; + +const ProgressBar = ( { percent } ) => { + percent = Math.min( Math.max( percent, 0 ), 100 ); + + return ( +
+ ); +}; + export default Navigation; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js index 30cd52ffe..225527053 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js @@ -1,40 +1,35 @@ import Container from '../../ReusableComponents/Container'; import { OnboardingHooks } from '../../../data'; -import { getSteps } from './availableSteps'; -import Navigation from './Components/Navigation'; - -const getCurrentStep = ( requestedStep, steps ) => { - const isValidStep = ( step ) => - typeof step === 'number' && - Number.isInteger( step ) && - step >= 0 && - step < steps.length; - const safeCurrentStep = isValidStep( requestedStep ) ? requestedStep : 0; - return steps[ safeCurrentStep ]; -}; +import { getSteps, getCurrentStep } from './availableSteps'; +import Navigation from './Components/Navigation'; const Onboarding = () => { - const { step, setStep, setCompleted, flags } = OnboardingHooks.useSteps(); - const steps = getSteps( flags ); + const { step, setStep, flags } = OnboardingHooks.useSteps(); + const Steps = getSteps( flags ); + const currentStep = getCurrentStep( step, Steps ); - const CurrentStepComponent = getCurrentStep( step, steps ); + const handleNext = () => setStep( currentStep.nextStep ); + const handlePrev = () => setStep( currentStep.prevStep ); + const handleExit = () => { + window.location.href = window.ppcpSettings.wcPaymentsTabUrl; + }; return ( <> +
-
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js index a223686ff..dd9a1dcd5 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js @@ -10,9 +10,8 @@ const BUSINESS_RADIO_GROUP_NAME = 'business'; const StepBusiness = ( {} ) => { const { isCasualSeller, setIsCasualSeller } = OnboardingHooks.useBusiness(); - const handleSellerTypeChange = ( value ) => { + const handleSellerTypeChange = ( value ) => setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === value ); - }; const getCurrentValue = () => { if ( isCasualSeller === null ) { diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js index 5f63f923e..ed2001ac2 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js @@ -1,28 +1,9 @@ import { __ } from '@wordpress/i18n'; -import { Button, Icon } from '@wordpress/components'; import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; +import ConnectionButton from './Components/ConnectionButton'; -const StepCompleteSetup = ( { setCompleted } ) => { - const ButtonIcon = () => ( - ( - - - - ) } - /> - ); - +const StepCompleteSetup = () => { return (
{ />
-
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js index a9d2f6b9e..e94f176f7 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js @@ -1,10 +1,12 @@ -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; import SelectBox from '../../ReusableComponents/SelectBox'; -import { OnboardingHooks } from '../../../data'; +import { CommonHooks, OnboardingHooks } from '../../../data'; import OptionalPaymentMethods from '../../ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods'; +import { pricesBasedDescription } from '../../ReusableComponents/WelcomeDocs/pricesBasedDescription'; +import { countryPriceInfo } from '../../../utils/countryPriceInfo'; const OPM_RADIO_GROUP_NAME = 'optional-payment-methods'; @@ -13,14 +15,8 @@ const StepPaymentMethods = ( {} ) => { areOptionalPaymentMethodsEnabled, setAreOptionalPaymentMethodsEnabled, } = OnboardingHooks.useOptionalPaymentMethods(); - const pricesBasedDescription = sprintf( - // translators: %s: Link to PayPal REST application guide - __( - '1Prices based on domestic transactions as of October 25th, 2024. Click here for full pricing details.', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - ); + + const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); return (
@@ -42,8 +38,8 @@ const StepPaymentMethods = ( {} ) => { useAcdc={ true } isFastlane={ true } isPayLater={ true } - storeCountry={ 'us' } - storeCurrency={ 'usd' } + storeCountry={ storeCountry } + storeCurrency={ storeCurrency } /> } name={ OPM_RADIO_GROUP_NAME } @@ -64,12 +60,14 @@ const StepPaymentMethods = ( {} ) => { type="radio" > -

+ { storeCountry in countryPriceInfo && ( +

+ ) }
); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js index cbd642327..ee99f4acf 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js @@ -9,6 +9,7 @@ const PRODUCTS_CHECKBOX_GROUP_NAME = 'products'; const StepProducts = () => { const { products, setProducts } = OnboardingHooks.useProducts(); + const { canUseSubscriptions } = OnboardingHooks.useFlags(); return (
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js index c94c84935..f8abf9ea5 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js @@ -8,8 +8,12 @@ import WelcomeDocs from '../../ReusableComponents/WelcomeDocs/WelcomeDocs'; import AccordionSection from '../../ReusableComponents/AccordionSection'; import AdvancedOptionsForm from './Components/AdvancedOptionsForm'; +import { CommonHooks } from '../../../data'; +import BusyStateWrapper from '../../ReusableComponents/BusyStateWrapper'; + +const StepWelcome = ( { setStep, currentStep } ) => { + const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); -const StepWelcome = ( { setStep, currentStep, setCompleted } ) => { return (
{ 'woocommerce-paypal-payments' ) }

- + + +
{ className="onboarding-advanced-options" id="advanced-options" > - +
); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js index 7e8ea1556..e14e66231 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js @@ -1,21 +1,86 @@ +import { __ } from '@wordpress/i18n'; + import StepWelcome from './StepWelcome'; import StepBusiness from './StepBusiness'; import StepProducts from './StepProducts'; import StepPaymentMethods from './StepPaymentMethods'; import StepCompleteSetup from './StepCompleteSetup'; +/** + * List of all onboarding screens that are available. + * + * The screens are displayed in the order in which they appear in this array + * + * @type {[{id, StepComponent, title}]} + */ +const ALL_STEPS = [ + { + id: 'welcome', + title: __( 'PayPal Payments', 'woocommerce-paypal-payments' ), + StepComponent: StepWelcome, + canProceed: () => true, + }, + { + id: 'business', + title: __( 'Set up store type', 'woocommerce-paypal-payments' ), + StepComponent: StepBusiness, + canProceed: ( { business } ) => business.isCasualSeller !== null, + }, + { + id: 'products', + title: __( 'Select product types', 'woocommerce-paypal-payments' ), + StepComponent: StepProducts, + canProceed: ( { products } ) => products.products.length > 0, + }, + { + id: 'methods', + title: __( 'Choose checkout options', 'woocommerce-paypal-payments' ), + StepComponent: StepPaymentMethods, + canProceed: () => true, + }, + { + id: 'complete', + title: __( + 'Connect your PayPal account', + 'woocommerce-paypal-payments' + ), + StepComponent: StepCompleteSetup, + canProceed: () => true, + }, +]; + export const getSteps = ( flags ) => { - const allSteps = [ - StepWelcome, - StepBusiness, - StepProducts, - StepPaymentMethods, - StepCompleteSetup, - ]; + const steps = flags.canUseCasualSelling + ? ALL_STEPS + : ALL_STEPS.filter( ( step ) => step.id !== 'business' ); + + const totalStepsCount = steps.length; + + return steps.map( ( step, index ) => ( { + ...step, + isFirst: index === 0, + isLast: index === totalStepsCount - 1, + showNext: index < totalStepsCount - 1, + percentage: 100 * ( index / ( totalStepsCount - 1 ) ), + nextStep: index < totalStepsCount - 1 ? index + 1 : index, + prevStep: index > 0 ? index - 1 : 0, + } ) ); +}; - if ( ! flags.canUseCasualSelling ) { - return allSteps.filter( ( step ) => step !== StepBusiness ); - } +/** + * Returns the screen-details of the current step, based on the numeric step-index. + * + * @param {number} requestedStep Index of the screen to display. + * @param {[]} steps List of all available steps (see `getSteps()`) + * @return {{id, StepComponent, title}} The requested screen details, or the first welcome screen. + */ +export const getCurrentStep = ( requestedStep, steps ) => { + const isValidStep = ( step ) => + typeof step === 'number' && + Number.isInteger( step ) && + step >= 0 && + step < steps.length; - return allSteps; + const safeCurrentStep = isValidStep( requestedStep ) ? requestedStep : 0; + return steps[ safeCurrentStep ]; }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js index 4bac75c9a..fe3e64218 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js @@ -1,18 +1,11 @@ -import SettingsCard from '../../ReusableComponents/SettingsCard'; import { __ } from '@wordpress/i18n'; -import { - PayPalCheckbox, -} from '../../ReusableComponents/Fields'; import { useState } from '@wordpress/element'; -import data from '../../../utils/data'; import { Button } from '@wordpress/components'; -import TitleBadge, { - TITLE_BADGE_NEGATIVE, - TITLE_BADGE_POSITIVE, -} from '../../ReusableComponents/TitleBadge'; -import ConnectionInfo, { - connectionStatusDataDefault, -} from '../../ReusableComponents/ConnectionInfo'; +import SettingsCard from '../../ReusableComponents/SettingsCard'; +import TodoSettingsBlock from '../../ReusableComponents/SettingsBlocks/TodoSettingsBlock'; +import FeatureSettingsBlock from '../../ReusableComponents/SettingsBlocks/FeatureSettingsBlock'; +import { TITLE_BADGE_POSITIVE } from '../../ReusableComponents/TitleBadge'; +import data from '../../../utils/data'; const TabOverview = () => { const [ todos, setTodos ] = useState( [] ); @@ -32,198 +25,52 @@ const TabOverview = () => { 'woocommerce-paypal-payments' ) } > -
- { todosData.map( ( todo ) => ( - - ) ) } -
+ ) } - - - - { featuresDefault.map( ( feature ) => { - return ( - - ); - } ) } - -
- ); -}; -const ConnectionStatus = ( { connectionData } ) => { - return ( -
-
-
- - { __( 'Connection', 'woocommerce-paypal-payments' ) } - - { connectionData.connectionStatus ? ( - - ) : ( - - ) } -
-
- - { __( - 'PayPal Account Details', - 'woocommerce-paypal-payments' - ) } - -
-
- { connectionData.connectionStatus && ( - - ) } -
- ); -}; - -const FeaturesRefresh = () => { - return ( -
-
- - { __( 'Features', 'woocommerce-paypal-payments' ) } - -

- { __( - 'After making changes to your PayPal account, click Refresh to update your store features.', - 'woocommerce-paypal-payments' - ) } -

-
- -
- ); -}; - -const TodoItem = ( props ) => { - return ( -
-
- { ' ' } -
-
- removeTodo( - props.value, - props.todosData, - props.changeTodos - ) + +

{ __( 'Enable additional features…' ) }

+

{ __( 'Click Refresh…' ) }

+ +
} - > - { data().getImage( 'icon-close.svg' ) } -
-
- ); -}; - -const FeatureItem = ( { feature } ) => { - const printNotes = () => { - if ( ! feature?.notes ) { - return null; - } - - if ( Array.isArray( feature.notes ) && feature.notes.length === 0 ) { - return null; - } - - return ( - <> -
-
- - { feature.notes.map( ( note, index ) => { - return { note }; - } ) } - - - ); - }; - - return ( -
- - { feature.title } - { feature?.featureStatus && ( - ( + - ) } - -

- { feature.description } - { printNotes() } -

-
- { feature.buttons.map( ( button ) => { - return ( - - ); - } ) } -
+ ) ) } + />
); }; -const removeTodo = ( todoValue, todosData, changeTodos ) => { - changeTodos( todosData.filter( ( todo ) => todo.value !== todoValue ) ); -}; - const todosDataDefault = [ { value: 'paypal_later_messaging', @@ -269,12 +116,12 @@ const featuresDefault = [ ), buttons: [ { - type: 'primary', + type: 'secondary', text: __( 'Configure', 'woocommerce-paypal-payments' ), url: '#', }, { - type: 'secondary', + type: 'tertiary', text: __( 'Learn more', 'woocommerce-paypal-payments' ), url: '#', }, @@ -293,12 +140,12 @@ const featuresDefault = [ ), buttons: [ { - type: 'primary', + type: 'secondary', text: __( 'Configure', 'woocommerce-paypal-payments' ), url: '#', }, { - type: 'secondary', + type: 'tertiary', text: __( 'Learn more', 'woocommerce-paypal-payments' ), url: '#', }, @@ -316,12 +163,12 @@ const featuresDefault = [ ), buttons: [ { - type: 'primary', + type: 'secondary', text: __( 'Apply', 'woocommerce-paypal-payments' ), url: '#', }, { - type: 'secondary', + type: 'tertiary', text: __( 'Learn more', 'woocommerce-paypal-payments' ), url: '#', }, @@ -337,12 +184,12 @@ const featuresDefault = [ featureStatus: true, buttons: [ { - type: 'primary', + type: 'secondary', text: __( 'Configure', 'woocommerce-paypal-payments' ), url: '#', }, { - type: 'secondary', + type: 'tertiary', text: __( 'Learn more', 'woocommerce-paypal-payments' ), url: '#', }, @@ -360,7 +207,7 @@ const featuresDefault = [ ), buttons: [ { - type: 'primary', + type: 'secondary', text: __( 'Domain registration', 'woocommerce-paypal-payments' @@ -368,7 +215,7 @@ const featuresDefault = [ url: '#', }, { - type: 'secondary', + type: 'tertiary', text: __( 'Learn more', 'woocommerce-paypal-payments' ), url: '#', }, @@ -383,16 +230,17 @@ const featuresDefault = [ ), buttons: [ { - type: 'primary', + type: 'secondary', text: __( 'Configure', 'woocommerce-paypal-payments' ), url: '#', }, { - type: 'secondary', + type: 'tertiary', text: __( 'Learn more', 'woocommerce-paypal-payments' ), url: '#', }, ], }, ]; + export default TabOverview; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js index 453a34426..c1576da10 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabPaymentMethods.js @@ -1,23 +1,34 @@ -import SettingsCard from '../../ReusableComponents/SettingsCard'; import { __ } from '@wordpress/i18n'; -import PaymentMethodItem from '../../ReusableComponents/PaymentMethodItem'; +import { useMemo } from '@wordpress/element'; + +import SettingsCard from '../../ReusableComponents/SettingsCard'; +import PaymentMethodsBlock from '../../ReusableComponents/SettingsBlocks/PaymentMethodsBlock'; +import { CommonHooks } from '../../../data'; import ModalPayPal from './Modals/ModalPayPal'; import ModalFastlane from './Modals/ModalFastlane'; import ModalAcdc from './Modals/ModalAcdc'; const TabPaymentMethods = () => { - const renderPaymentMethods = ( data ) => { - return ( -
- { data.map( ( paymentMethod ) => ( - - ) ) } -
- ); - }; + const { storeCountry, storeCurrency } = CommonHooks.useWooSettings(); + + const filteredPaymentMethods = useMemo( () => { + const contextProps = { storeCountry, storeCurrency }; + + return { + payPalCheckout: filterPaymentMethods( + paymentMethodsPayPalCheckout, + contextProps + ), + onlineCardPayments: filterPaymentMethods( + paymentMethodsOnlineCardPayments, + contextProps + ), + alternative: filterPaymentMethods( + paymentMethodsAlternative, + contextProps + ), + }; + }, [ storeCountry, storeCurrency ] ); return (
@@ -28,8 +39,11 @@ const TabPaymentMethods = () => { 'woocommerce-paypal-payments' ) } icon="icon-checkout-standard.svg" + contentContainer={ false } > - { renderPaymentMethods( paymentMethodsPayPalCheckoutDefault ) } + { 'woocommerce-paypal-payments' ) } icon="icon-checkout-online-methods.svg" + contentContainer={ false } > - { renderPaymentMethods( - paymentMethodsOnlineCardPaymentsDefault - ) } + { 'woocommerce-paypal-payments' ) } icon="icon-checkout-alternative-methods.svg" + contentContainer={ false } > - { renderPaymentMethods( paymentMethodsAlternativeDefault ) } +
); }; -const paymentMethodsPayPalCheckoutDefault = [ +function filterPaymentMethods( paymentMethods, contextProps ) { + return paymentMethods.filter( ( method ) => + typeof method.condition === 'function' + ? method.condition( contextProps ) + : true + ); +} + +const paymentMethodsPayPalCheckout = [ { id: 'paypal', title: __( 'PayPal', 'woocommerce-paypal-payments' ), @@ -106,7 +132,7 @@ const paymentMethodsPayPalCheckoutDefault = [ }, ]; -const paymentMethodsOnlineCardPaymentsDefault = [ +const paymentMethodsOnlineCardPayments = [ { id: 'advanced_credit_and_debit_card_payments', title: __( @@ -124,7 +150,7 @@ const paymentMethodsOnlineCardPaymentsDefault = [ id: 'fastlane', title: __( 'Fastlane by PayPal', 'woocommerce-paypal-payments' ), description: __( - 'Tap into the scale and trust of PayPal’s customer network to recognize shoppers and make guest checkout more seamless than ever.', + "Tap into the scale and trust of PayPal's customer network to recognize shoppers and make guest checkout more seamless than ever.", 'woocommerce-paypal-payments' ), icon: 'payment-method-fastlane', @@ -150,7 +176,7 @@ const paymentMethodsOnlineCardPaymentsDefault = [ }, ]; -const paymentMethodsAlternativeDefault = [ +const paymentMethodsAlternative = [ { id: 'bancontact', title: __( 'Bancontact', 'woocommerce-paypal-payments' ), @@ -173,7 +199,7 @@ const paymentMethodsAlternativeDefault = [ id: 'eps', title: __( 'eps', 'woocommerce-paypal-payments' ), description: __( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum porttitor massa ex, eget luctus lacus iaculis at.', + 'An online payment method in Austria, enabling Austrian buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.', 'woocommerce-paypal-payments' ), icon: 'payment-method-eps', @@ -182,11 +208,69 @@ const paymentMethodsAlternativeDefault = [ id: 'blik', title: __( 'BLIK', 'woocommerce-paypal-payments' ), description: __( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum porttitor massa ex, eget luctus lacus iaculis at.', + 'A widely used mobile payment method in Poland, allowing Polish customers to pay directly via their banking apps. Transactions are processed in PLN.', 'woocommerce-paypal-payments' ), icon: 'payment-method-blik', }, + { + id: 'mybank', + title: __( 'MyBank', 'woocommerce-paypal-payments' ), + description: __( + 'A European online banking payment solution primarily used in Italy, enabling customers to make secure bank transfers during checkout. Transactions are processed in EUR.', + 'woocommerce-paypal-payments' + ), + icon: 'payment-method-mybank', + }, + { + id: 'przelewy24', + title: __( 'Przelewy24', 'woocommerce-paypal-payments' ), + description: __( + 'A popular online payment gateway in Poland, offering various payment options for Polish customers. Transactions can be processed in PLN or EUR.', + 'woocommerce-paypal-payments' + ), + icon: 'payment-method-przelewy24', + }, + { + id: 'trustly', + title: __( 'Trustly', 'woocommerce-paypal-payments' ), + description: __( + 'A European payment method that allows buyers to make payments directly from their bank accounts, suitable for customers across multiple European countries. Supported currencies include EUR, DKK, SEK, GBP, and NOK.', + 'woocommerce-paypal-payments' + ), + icon: 'payment-method-trustly', + }, + { + id: 'multibanco', + title: __( 'Multibanco', 'woocommerce-paypal-payments' ), + description: __( + 'An online payment method in Portugal, enabling Portuguese buyers to make secure payments directly through their bank accounts. Transactions are processed in EUR.', + 'woocommerce-paypal-payments' + ), + icon: 'payment-method-multibanco', + }, + { + id: 'pui', + title: __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ), + description: __( + 'Pay upon Invoice is an invoice payment method in Germany. It is a local buy now, pay later payment method that allows the buyer to place an order, receive the goods, try them, verify they are in good order, and then pay the invoice within 30 days.', + 'woocommerce-paypal-payments' + ), + icon: 'payment-method-ratepay', + condition: ( { storeCountry, storeCurrency } ) => + storeCountry === 'DE' && storeCurrency === 'EUR', + }, + { + id: 'oxxo', + title: __( 'OXXO', 'woocommerce-paypal-payments' ), + description: __( + 'OXXO is a Mexican chain of convenience stores. *Get PayPal account permission to use OXXO payment functionality by contacting us at (+52) 800–925–0304', + 'woocommerce-paypal-payments' + ), + icon: 'payment-method-oxxo', + condition: ( { storeCountry, storeCurrency } ) => + storeCountry === 'MX' && storeCurrency === 'MXN', + }, ]; export default TabPaymentMethods; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js index 5505fac8d..1b471fe1e 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js @@ -1,4 +1,5 @@ import { useState } from '@wordpress/element'; +import ConnectionStatus from './TabSettingsElements/ConnectionStatus'; import CommonSettings from './TabSettingsElements/CommonSettings'; import ExpertSettings from './TabSettingsElements/ExpertSettings'; @@ -30,6 +31,7 @@ const TabSettings = () => { return ( <>
+ { return ( - + components={ [ + () => ( + <> +
+ + { __( + 'Order Intent', + 'woocommerce-paypal-payments' + ) } + + + { __( + 'Choose between immediate capture or authorization-only, with manual capture in the Order section.', + 'woocommerce-paypal-payments' + ) } + +
+ + ), + () => ( + <> + - - + + + ), + ] } + /> ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/OtherSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/OtherSettings.js index 84bea84c8..a377f0217 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/OtherSettings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/OtherSettings.js @@ -1,66 +1,59 @@ -import SettingsBlock, { - SETTINGS_BLOCK_STYLING_TYPE_PRIMARY, - SETTINGS_BLOCK_STYLING_TYPE_SECONDARY, - SETTINGS_BLOCK_TYPE_SELECT, - SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT, -} from '../../../../ReusableComponents/SettingsBlock'; import { __ } from '@wordpress/i18n'; +import { + AccordionSettingsBlock, + SelectSettingsBlock, +} from '../../../../ReusableComponents/SettingsBlocks'; + +const creditCardExamples = [ + { value: '', label: __( 'Select', 'woocommerce-paypal-payments' ) }, + { + value: 'mastercard', + label: __( 'Mastercard', 'woocommerce-paypal-payments' ), + }, + { value: 'visa', label: __( 'Visa', 'woocommerce-paypal-payments' ) }, + { + value: 'amex', + label: __( 'American Express', 'woocommerce-paypal-payments' ), + }, + { value: 'jcb', label: __( 'JCB', 'woocommerce-paypal-payments' ) }, + { + value: 'diners-club', + label: __( 'Diners Club', 'woocommerce-paypal-payments' ), + }, +]; const OtherSettings = ( { settings, updateFormValue } ) => { return ( - - - + ); }; -const creditCardExamples = [ - { value: '', label: __( 'Select', 'woocommerce-paypal-payments' ) }, - { - value: 'mastercard', - label: __( 'Mastercard', 'woocommerce-paypal-payments' ), - }, - { value: 'visa', label: __( 'Visa', 'woocommerce-paypal-payments' ) }, - { - value: 'amex', - label: __( 'American Express', 'woocommerce-paypal-payments' ), - }, - { value: 'jcb', label: __( 'JCB', 'woocommerce-paypal-payments' ) }, - { - value: 'diners-club', - label: __( 'Diners Club', 'woocommerce-paypal-payments' ), - }, -]; - export default OtherSettings; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaypalSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaypalSettings.js index 7b01ea203..f8d68881e 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaypalSettings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/PaypalSettings.js @@ -1,33 +1,28 @@ import { __ } from '@wordpress/i18n'; -import SettingsBlock, { - SETTINGS_BLOCK_STYLING_TYPE_PRIMARY, - SETTINGS_BLOCK_STYLING_TYPE_SECONDARY, - SETTINGS_BLOCK_TYPE_EMPTY, - SETTINGS_BLOCK_TYPE_INPUT, - SETTINGS_BLOCK_TYPE_SELECT, - SETTINGS_BLOCK_TYPE_TOGGLE, - SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT, -} from '../../../../ReusableComponents/SettingsBlock'; -import { PayPalRdbWithContent } from '../../../../ReusableComponents/Fields'; +import { + AccordionSettingsBlock, + RadioSettingsBlock, + ToggleSettingsBlock, + InputSettingsBlock, + SelectSettingsBlock, +} from '../../../../ReusableComponents/SettingsBlocks'; const PaypalSettings = ( { updateFormValue, settings } ) => { return ( - - { 'Due to differences in how WooCommerce and PayPal calculates taxes, some transactions may fail due to a rounding error. This settings determines the fallback behavior.', 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } - actionProps={ { - type: SETTINGS_BLOCK_TYPE_EMPTY, - } } - > -
- - updateFormValue( - 'subtotalMismatchFallback', - newValue - ) - } - label={ __( + options={ [ + { + id: 'add_a_correction', + value: 'add_a_correction', + label: __( 'Add a correction', 'woocommerce-paypal-payments' - ) } - description={ __( + ), + description: __( 'Adds an additional line item with the missing amount.', 'woocommerce-paypal-payments' - ) } - /> - - updateFormValue( - 'subtotalMismatchFallback', - newValue - ) - } - label={ __( + ), + }, + { + id: 'do_not_send_line_items', + value: 'do_not_send_line_items', + label: __( 'Do not send line items', 'woocommerce-paypal-payments' - ) } - description={ __( - 'Resubmit the transaction without line item details', + ), + description: __( + 'Resubmit the transaction without line item details.', 'woocommerce-paypal-payments' - ) } - /> -
-
+ ), + }, + ] } + actionProps={ { + name: 'paypal_settings_mismatch', + key: 'subtotalMismatchFallback', + currentValue: settings.subtotalMismatchFallback, + callback: updateFormValue, + } } + /> - { 'If enabled, PayPal will not allow buyers to use funding sources that take additional time to complete, such as eChecks.', 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } actionProps={ { - type: SETTINGS_BLOCK_TYPE_TOGGLE, value: settings.savePaypalAndVenmo, callback: updateFormValue, key: 'savePaypalAndVenmo', } } /> - { 'woocommerce-paypal-payments' ), } } + order={ [ 'title', 'description', 'action' ] } /> - { 'woocommerce-paypal-payments' ), } } + order={ [ 'title', 'description', 'action' ] } /> - { 'Determine which experience a buyer sees when they click the PayPal button.', 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } - actionProps={ { - type: SETTINGS_BLOCK_TYPE_EMPTY, - } } - > -
- - updateFormValue( 'paypalLandingPage', newValue ) - } - label={ __( + options={ [ + { + id: 'no_preference', + value: 'no_reference', + label: __( 'No preference', 'woocommerce-paypal-payments' - ) } - description={ __( + ), + description: __( 'Shows the buyer the PayPal login for a recognized PayPal buyer.', 'woocommerce-paypal-payments' - ) } - /> - - updateFormValue( 'paypalLandingPage', newValue ) - } - label={ __( + ), + }, + { + id: 'login_page', + value: 'login_page', + label: __( 'Login page', 'woocommerce-paypal-payments' - ) } - description={ __( + ), + description: __( 'Always show the buyer the PayPal login screen.', 'woocommerce-paypal-payments' - ) } - /> - - updateFormValue( 'paypalLandingPage', newValue ) - } - label={ __( + ), + }, + { + id: 'guest_checkout_page', + value: 'guest_checkout_page', + label: __( 'Guest checkout page', 'woocommerce-paypal-payments' - ) } - description={ __( + ), + description: __( 'Always show the buyer the guest checkout fields first.', 'woocommerce-paypal-payments' - ) } - /> -
-
- + + { 'woocommerce-paypal-payments' ), } } + order={ [ 'title', 'description', 'action' ] } /> - + ); }; @@ -235,4 +200,5 @@ const languagesExample = [ { value: 'es', label: 'Spanish' }, { value: 'it', label: 'Italian' }, ]; + export default PaypalSettings; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Sandbox.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Sandbox.js index f47711098..93a4a7d0d 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Sandbox.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Sandbox.js @@ -1,43 +1,40 @@ import { __, sprintf } from '@wordpress/i18n'; -import SettingsBlock, { - SETTINGS_BLOCK_STYLING_TYPE_PRIMARY, - SETTINGS_BLOCK_STYLING_TYPE_SECONDARY, - SETTINGS_BLOCK_STYLING_TYPE_TERTIARY, - SETTINGS_BLOCK_TYPE_EMPTY, - SETTINGS_BLOCK_TYPE_TOGGLE, - SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT, -} from '../../../../ReusableComponents/SettingsBlock'; +import { Button } from '@wordpress/components'; +import { + AccordionSettingsBlock, + ButtonSettingsBlock, + RadioSettingsBlock, + ToggleSettingsBlock, + InputSettingsBlock, +} from '../../../../ReusableComponents/SettingsBlocks'; import TitleBadge, { TITLE_BADGE_POSITIVE, } from '../../../../ReusableComponents/TitleBadge'; import ConnectionInfo, { connectionStatusDataDefault, } from '../../../../ReusableComponents/ConnectionInfo'; -import { Button, TextControl } from '@wordpress/components'; -import { PayPalRdbWithContent } from '../../../../ReusableComponents/Fields'; const Sandbox = ( { settings, updateFormValue } ) => { const className = settings.sandboxConnected ? 'ppcp-r-settings-block--sandbox-connected' : 'ppcp-r-settings-block--sandbox-disconnected'; + return ( - Note: No real payments/money movement occur in Sandbox mode. Do not ship orders made in this mode.", + "Test your site in PayPal's Sandbox environment.", 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_PRIMARY } actionProps={ { - type: SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT, callback: updateFormValue, key: 'payNowExperience', value: settings.payNowExperience, } } > { settings.sandboxConnected && ( - { ) } /> } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } - actionProps={ { - type: SETTINGS_BLOCK_TYPE_EMPTY, - callback: updateFormValue, - key: 'sandboxAccountCredentials', - value: settings.sandboxAccountCredentials, - } } >
- { ) }
-
+ ) } { ! settings.sandboxConnected && ( - { 'Connect a PayPal Sandbox account in order to test your website. Transactions made will not result in actual money movement. Do not fulfil orders completed in Sandbox mode.', 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } - actionProps={ { - type: SETTINGS_BLOCK_TYPE_EMPTY, - callback: updateFormValue, - key: 'sandboxAccountCredentials', - value: settings.sandboxAccountCredentials, - } } - > -
- - updateFormValue( 'sandboxMode', newValue ) - } - label={ __( + options={ [ + { + id: 'sandbox_mode', + value: 'sandbox_mode', + label: __( 'Sandbox Mode', 'woocommerce-paypal-payments' - ) } - description={ __( + ), + description: __( 'Activate Sandbox mode to safely test PayPal with sample data. Once your store is ready to go live, you can easily switch to your production account.', 'woocommerce-paypal-payments' - ) } - > - - - - updateFormValue( 'sandboxMode', newValue ) - } - label={ __( + ), + additionalContent: ( + + ), + }, + { + id: 'manual_connect', + value: 'manual_connect', + label: __( 'Manual Connect', 'woocommerce-paypal-payments' - ) } - description={ sprintf( - // translators: %s: Link to creating PayPal REST application + ), + description: sprintf( __( 'For advanced users: Connect a custom PayPal REST app for full control over your integration. For more information on creating a PayPal REST application, click here.', 'woocommerce-paypal-payments' ), '#' - ) } - > - - - - -
-
+ ), + additionalContent: ( + <> + + + + + ), + }, + ] } + actionProps={ { + name: 'paypal_connect_sandbox', + key: 'sandboxMode', + currentValue: settings.sandboxMode, + callback: updateFormValue, + } } + /> ) } -
+ ); }; + export default Sandbox; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/SavePaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/SavePaymentMethods.js index 8fdefbb9c..f10907c4c 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/SavePaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/SavePaymentMethods.js @@ -1,75 +1,83 @@ -import SettingsBlock, { - SETTINGS_BLOCK_STYLING_TYPE_PRIMARY, - SETTINGS_BLOCK_STYLING_TYPE_SECONDARY, - SETTINGS_BLOCK_TYPE_EMPTY, - SETTINGS_BLOCK_TYPE_TOGGLE, -} from '../../../../ReusableComponents/SettingsBlock'; import { __, sprintf } from '@wordpress/i18n'; +import { + SettingsBlock, + ToggleSettingsBlock, + Title, + Description, +} from '../../../../ReusableComponents/SettingsBlocks'; +import { Header } from '../../../../ReusableComponents/SettingsBlocks/SettingsBlockElements'; const SavePaymentMethods = ( { updateFormValue, settings } ) => { return ( future payments[MISSING_LINK] and subscriptions[MISSING_LINK], simplifying checkout and enabling recurring transactions.', - 'woocommerce-paypal-payments' + components={ [ + () => ( + <> +
+ + { __( + 'Save payment methods', + 'woocommerce-paypal-payments' + ) } + + + { __( + 'Securely store customers’ payment methods for future payments and subscriptions, simplifying checkout and enabling recurring transactions.', + 'woocommerce-paypal-payments' + ) } + +
+ ), - '#', - '#' - ) } - type={ SETTINGS_BLOCK_STYLING_TYPE_PRIMARY } - style={ SETTINGS_BLOCK_STYLING_TYPE_PRIMARY } - actionProps={ { - type: SETTINGS_BLOCK_TYPE_EMPTY, - } } - > - This will disable all Pay Later features and Alternative Payment Methods on your site.', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later', - 'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods' - ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } - value={ settings.savePaypalAndVenmo } - actionProps={ { - type: SETTINGS_BLOCK_TYPE_TOGGLE, - value: settings.savePaypalAndVenmo, - callback: updateFormValue, - key: 'savePaypalAndVenmo', - } } - /> - - + () => ( + This will disable all Pay Later features and Alternative Payment Methods on your site.', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later', + 'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods' + ), + } } + /> + } + actionProps={ { + value: settings.savePaypalAndVenmo, + callback: updateFormValue, + key: 'savePaypalAndVenmo', + } } + /> + ), + () => ( + + ), + ] } + /> ); }; + export default SavePaymentMethods; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js index fdc4ad28f..f53a360c7 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/Blocks/Troubleshooting.js @@ -1,65 +1,73 @@ -import SettingsBlock, { - SETTINGS_BLOCK_STYLING_TYPE_PRIMARY, - SETTINGS_BLOCK_STYLING_TYPE_SECONDARY, - SETTINGS_BLOCK_TYPE_BUTTON, - SETTINGS_BLOCK_TYPE_EMPTY, - SETTINGS_BLOCK_TYPE_TOGGLE, - SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT, -} from '../../../../ReusableComponents/SettingsBlock'; import { __ } from '@wordpress/i18n'; +import { + Header, + Title, + Description, + AccordionSettingsBlock, + ToggleSettingsBlock, + ButtonSettingsBlock, +} from '../../../../ReusableComponents/SettingsBlocks'; +import SettingsBlock from '../../../../ReusableComponents/SettingsBlocks/SettingsBlock'; const Troubleshooting = ( { updateFormValue, settings } ) => { return ( - - - - + components={ [ + () => ( + <> +
+ + { __( + 'Subscribed PayPal webhooks', + 'woocommerce-paypal-payments' + ) } + + + { __( + 'The following PayPal webhooks are subscribed. More information about the webhooks is available in the', + 'woocommerce-paypal-payments' + ) }{ ' ' } + + { __( + 'Webhook Status documentation', + 'woocommerce-paypal-payments' + ) } + + . + +
+ + + ), + ] } + /> - { 'Click to remove the current webhook subscription and subscribe again, for example, if the website domain or URL structure changed.', 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } actionProps={ { - type: SETTINGS_BLOCK_TYPE_BUTTON, buttonType: 'secondary', callback: () => console.log( @@ -83,14 +89,13 @@ const Troubleshooting = ( { updateFormValue, settings } ) => { ), } } /> - console.log( @@ -103,7 +108,7 @@ const Troubleshooting = ( { updateFormValue, settings } ) => { ), } } /> - + ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/CommonSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/CommonSettings.js index 0066a5fcd..dad5da83e 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/CommonSettings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/CommonSettings.js @@ -1,10 +1,8 @@ import { __ } from '@wordpress/i18n'; -import SettingsBlock, { - SETTINGS_BLOCK_STYLING_TYPE_PRIMARY, - SETTINGS_BLOCK_STYLING_TYPE_SECONDARY, - SETTINGS_BLOCK_TYPE_INPUT, - SETTINGS_BLOCK_TYPE_TOGGLE, -} from '../../../ReusableComponents/SettingsBlock'; +import { + InputSettingsBlock, + ToggleSettingsBlock, +} from '../../../ReusableComponents/SettingsBlocks'; import SettingsCard from '../../../ReusableComponents/SettingsCard'; import OrderIntent from './Blocks/OrderIntent'; import SavePaymentMethods from './Blocks/SavePaymentMethods'; @@ -13,19 +11,21 @@ const CommonSettings = ( { updateFormValue, settings } ) => { return ( - { ), } } /> + + - { 'Let PayPal customers skip the Order Review page by selecting shipping options directly within PayPal.', 'woocommerce-paypal-payments' ) } - style={ SETTINGS_BLOCK_STYLING_TYPE_SECONDARY } actionProps={ { - type: SETTINGS_BLOCK_TYPE_TOGGLE, callback: updateFormValue, key: 'payNowExperience', value: settings.payNowExperience, diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ConnectionStatus.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ConnectionStatus.js new file mode 100644 index 000000000..b1018d44c --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ConnectionStatus.js @@ -0,0 +1,53 @@ +import { __ } from '@wordpress/i18n'; +import SettingsCard from '../../../ReusableComponents/SettingsCard'; +import ConnectionInfo, { + connectionStatusDataDefault, +} from '../../../ReusableComponents/ConnectionInfo'; +import TitleBadge, { + TITLE_BADGE_NEGATIVE, + TITLE_BADGE_POSITIVE, +} from '../../../ReusableComponents/TitleBadge'; +const ConnectionStatus = () => { + return ( + +
+
+
+ { connectionStatusDataDefault.connectionStatus ? ( + + ) : ( + + ) } +
+
+ { connectionStatusDataDefault.connectionStatus && ( + + ) } +
+
+ ); +}; +export default ConnectionStatus; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ExpertSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ExpertSettings.js index 78e8bcd20..56a8e63c6 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ExpertSettings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettingsElements/ExpertSettings.js @@ -1,11 +1,9 @@ import { __ } from '@wordpress/i18n'; -import SettingsBlock, { - SETTINGS_BLOCK_STYLING_TYPE_PRIMARY, - SETTINGS_BLOCK_STYLING_TYPE_SECONDARY, - SETTINGS_BLOCK_TYPE_SELECT, - SETTINGS_BLOCK_TYPE_TOGGLE_CONTENT, -} from '../../../ReusableComponents/SettingsBlock'; import SettingsCard from '../../../ReusableComponents/SettingsCard'; +import { + Content, + ContentWrapper, +} from '../../../ReusableComponents/SettingsBlocks'; import Sandbox from './Blocks/Sandbox'; import Troubleshooting from './Blocks/Troubleshooting'; import PaypalSettings from './Blocks/PaypalSettings'; @@ -25,25 +23,37 @@ const ExpertSettings = ( { updateFormValue, settings } ) => { callback: updateFormValue, key: 'payNowExperience', } } + contentContainer={ false } > - + + + + - + + + - - + + + + + + + +
); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js index e0634343c..bc79b34b3 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js @@ -1,20 +1,51 @@ +import { useEffect, useMemo } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import classNames from 'classnames'; + import { OnboardingHooks } from '../../data'; +import SpinnerOverlay from '../ReusableComponents/SpinnerOverlay'; + import Onboarding from './Onboarding/Onboarding'; import SettingsScreen from './SettingsScreen'; const Settings = () => { const onboardingProgress = OnboardingHooks.useSteps(); - if ( ! onboardingProgress.isReady ) { - // TODO: Use better loading state indicator. - return
Loading...
; - } + // Disable the "Changes you made might not be saved" browser warning. + useEffect( () => { + const suppressBeforeUnload = ( event ) => { + event.stopImmediatePropagation(); + return undefined; + }; + + window.addEventListener( 'beforeunload', suppressBeforeUnload ); + + return () => { + window.removeEventListener( 'beforeunload', suppressBeforeUnload ); + }; + }, [] ); + + const wrapperClass = classNames( 'ppcp-r-app', { + loading: ! onboardingProgress.isReady, + } ); + + const Content = useMemo( () => { + if ( ! onboardingProgress.isReady ) { + return ( + + ); + } + + if ( ! onboardingProgress.completed ) { + return ; + } - if ( ! onboardingProgress.completed ) { - return ; - } + return ; + }, [ onboardingProgress ] ); - return ; + return
{ Content }
; }; export default Settings; diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js index 47de76afe..34e831508 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -10,10 +10,17 @@ export default { // Persistent data. SET_PERSISTENT: 'COMMON:SET_PERSISTENT', + RESET: 'COMMON:RESET', HYDRATE: 'COMMON:HYDRATE', + // Activity management (advanced solution that replaces the isBusy state). + START_ACTIVITY: 'COMMON:START_ACTIVITY', + STOP_ACTIVITY: 'COMMON:STOP_ACTIVITY', + // Controls - always start with "DO_". DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA', DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION', DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN', + DO_PRODUCTION_LOGIN: 'COMMON:DO_PRODUCTION_LOGIN', + DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT', }; diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 619aaca5f..7dd13206e 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -18,6 +18,13 @@ import { STORE_NAME } from './constants'; * @property {Object?} payload - Optional payload for the action. */ +/** + * Special. Resets all values in the onboarding store to initial defaults. + * + * @return {Action} The action. + */ +export const reset = () => ( { type: ACTION_TYPES.RESET } ); + /** * Persistent. Set the full onboarding details, usually during app initialization. * @@ -52,14 +59,35 @@ export const setIsSaving = ( isSaving ) => ( { } ); /** - * Transient. Changes the "manual connection is busy" flag. + * Transient (Activity): Marks the start of an async activity + * Think of it as "setIsBusy(true)" + * + * @param {string} id Internal ID/key of the action, used to stop it again. + * @param {?string} description Optional, description for logging/debugging + * @return {?Action} The action. + */ +export const startActivity = ( id, description = null ) => { + if ( ! id || 'string' !== typeof id ) { + console.warn( 'Activity ID must be a non-empty string' ); + return null; + } + + return { + type: ACTION_TYPES.START_ACTIVITY, + payload: { id, description }, + }; +}; + +/** + * Transient (Activity): Marks the end of an async activity. + * Think of it as "setIsBusy(false)" * - * @param {boolean} isBusy + * @param {string} id Internal ID/key of the action, used to stop it again. * @return {Action} The action. */ -export const setIsBusy = ( isBusy ) => ( { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { isBusy }, +export const stopActivity = ( id ) => ( { + type: ACTION_TYPES.STOP_ACTIVITY, + payload: { id }, } ); /** @@ -118,17 +146,22 @@ export const persist = function* () { }; /** - * Side effect. Initiates the sandbox login ISU. + * Side effect. Fetches the ISU-login URL for a sandbox account. * * @return {Action} The action. */ -export const connectViaSandbox = function* () { - yield setIsBusy( true ); - - const result = yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN }; - yield setIsBusy( false ); +export const connectToSandbox = function* () { + return yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN }; +}; - return result; +/** + * Side effect. Fetches the ISU-login URL for a production account. + * + * @param {string[]} products Which products/features to display in the ISU popup. + * @return {Action} The action. + */ +export const connectToProduction = function* ( products = [] ) { + return yield { type: ACTION_TYPES.DO_PRODUCTION_LOGIN, products }; }; /** @@ -140,15 +173,19 @@ export const connectViaIdAndSecret = function* () { const { clientId, clientSecret, useSandbox } = yield select( STORE_NAME ).persistentData(); - yield setIsBusy( true ); - - const result = yield { + return yield { type: ACTION_TYPES.DO_MANUAL_CONNECTION, clientId, clientSecret, useSandbox, }; - yield setIsBusy( false ); +}; - return result; +/** + * Side effect. Clears and refreshes the merchant data via a REST request. + * + * @return {Action} The action. + */ +export const refreshMerchantData = function* () { + return yield { type: ACTION_TYPES.DO_REFRESH_MERCHANT }; }; diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index c7ea9b4c1..9499ef069 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -8,7 +8,7 @@ export const STORE_NAME = 'wc/paypal/common'; /** - * REST path to hydrate data of this module by loading data from the WP DB.. + * REST path to hydrate data of this module by loading data from the WP DB. * * Used by resolvers. * @@ -16,6 +16,15 @@ export const STORE_NAME = 'wc/paypal/common'; */ export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/common'; +/** + * REST path to fetch merchant details from the WordPress DB. + * + * Used by controls. + * + * @type {string} + */ +export const REST_HYDRATE_MERCHANT_PATH = '/wc/v3/wc_paypal/common/merchant'; + /** * REST path to persist data of this module to the WP DB. * @@ -36,11 +45,11 @@ export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common'; export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual'; /** - * REST path to generate an ISU URL for the sandbox-login. + * REST path to generate an ISU URL for the PayPal-login. * * Used by: Controls * See: LoginLinkRestEndpoint.php * * @type {string} */ -export const REST_SANDBOX_CONNECTION_PATH = '/wc/v3/wc_paypal/login_link'; +export const REST_CONNECTION_URL_PATH = '/wc/v3/wc_paypal/login_link'; diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index 6de513e0b..7845f335f 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -7,12 +7,15 @@ * @file */ +import { dispatch } from '@wordpress/data'; import apiFetch from '@wordpress/api-fetch'; import { + STORE_NAME, REST_PERSIST_PATH, REST_MANUAL_CONNECTION_PATH, - REST_SANDBOX_CONNECTION_PATH, + REST_CONNECTION_URL_PATH, + REST_HYDRATE_MERCHANT_PATH, } from './constants'; import ACTION_TYPES from './action-types'; @@ -34,11 +37,33 @@ export const controls = { try { result = await apiFetch( { - path: REST_SANDBOX_CONNECTION_PATH, + path: REST_CONNECTION_URL_PATH, method: 'POST', data: { environment: 'sandbox', - products: [ 'EXPRESS_CHECKOUT' ], + products: [ 'EXPRESS_CHECKOUT' ], // Sandbox always uses EXPRESS_CHECKOUT. + }, + } ); + } catch ( e ) { + result = { + success: false, + error: e, + }; + } + + return result; + }, + + async [ ACTION_TYPES.DO_PRODUCTION_LOGIN ]( { products } ) { + let result = null; + + try { + result = await apiFetch( { + path: REST_CONNECTION_URL_PATH, + method: 'POST', + data: { + environment: 'production', + products, }, } ); } catch ( e ) { @@ -77,4 +102,23 @@ export const controls = { return result; }, + + async [ ACTION_TYPES.DO_REFRESH_MERCHANT ]() { + let result = null; + + try { + result = await apiFetch( { path: REST_HYDRATE_MERCHANT_PATH } ); + + if ( result.success && result.merchant ) { + await dispatch( STORE_NAME ).hydrate( result ); + } + } catch ( e ) { + result = { + success: false, + error: e, + }; + } + + return result; + }, }; diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 8be3857b0..8eaaa3924 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -31,7 +31,8 @@ const useHooks = () => { setManualConnectionMode, setClientId, setClientSecret, - connectViaSandbox, + connectToSandbox, + connectToProduction, connectViaIdAndSecret, } = useDispatch( STORE_NAME ); @@ -44,6 +45,15 @@ const useHooks = () => { const isSandboxMode = usePersistent( 'useSandbox' ); const isManualConnectionMode = usePersistent( 'useManualConnection' ); + const merchant = useSelect( + ( select ) => select( STORE_NAME ).merchant(), + [] + ); + const wooSettings = useSelect( + ( select ) => select( STORE_NAME ).wooSettings(), + [] + ); + const savePersistent = async ( setter, value ) => { setter( value ); await persist(); @@ -67,25 +77,24 @@ const useHooks = () => { setClientSecret: ( value ) => { return savePersistent( setClientSecret, value ); }, - connectViaSandbox, + connectToSandbox, + connectToProduction, connectViaIdAndSecret, + merchant, + wooSettings, }; }; -export const useBusyState = () => { - const { setIsBusy } = useDispatch( STORE_NAME ); - const isBusy = useTransient( 'isBusy' ); +export const useSandbox = () => { + const { isSandboxMode, setSandboxMode, connectToSandbox } = useHooks(); - return { - isBusy, - setIsBusy: useCallback( ( busy ) => setIsBusy( busy ), [ setIsBusy ] ), - }; + return { isSandboxMode, setSandboxMode, connectToSandbox }; }; -export const useSandbox = () => { - const { isSandboxMode, setSandboxMode, connectViaSandbox } = useHooks(); +export const useProduction = () => { + const { connectToProduction } = useHooks(); - return { isSandboxMode, setSandboxMode, connectViaSandbox }; + return { connectToProduction }; }; export const useManualConnection = () => { @@ -109,3 +118,64 @@ export const useManualConnection = () => { connectViaIdAndSecret, }; }; + +export const useWooSettings = () => { + const { wooSettings } = useHooks(); + + return wooSettings; +}; + +export const useMerchantInfo = () => { + const { merchant } = useHooks(); + const { refreshMerchantData } = useDispatch( STORE_NAME ); + + const verifyLoginStatus = useCallback( async () => { + const result = await refreshMerchantData(); + + if ( ! result.success ) { + throw new Error( result?.message || result?.error?.message ); + } + + // Verify if the server state is "connected" and we have a merchant ID. + return merchant?.isConnected && merchant?.id; + }, [ refreshMerchantData, merchant ] ); + + return { + merchant, // Merchant details + verifyLoginStatus, // Callback + }; +}; + +// -- Not using the `useHooks()` data provider -- + +export const useBusyState = () => { + const { startActivity, stopActivity } = useDispatch( STORE_NAME ); + + // Resolved value (object), contains a list of all running actions. + const activities = useSelect( + ( select ) => select( STORE_NAME ).getActivityList(), + [] + ); + + // Derive isBusy state from activities + const isBusy = Object.keys( activities ).length > 0; + + // HOC that starts and stops an activity while the callback is executed. + const withActivity = useCallback( + async ( id, description, asyncFn ) => { + startActivity( id, description ); + try { + return await asyncFn(); + } finally { + stopActivity( id ); + } + }, + [ startActivity, stopActivity ] + ); + + return { + withActivity, // HOC + isBusy, // Boolean. + activities, // Object. + }; +}; diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js index 3f822468b..7d3f5697f 100644 --- a/modules/ppcp-settings/resources/js/data/common/reducer.js +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -12,17 +12,30 @@ import ACTION_TYPES from './action-types'; // Store structure. -const defaultTransient = { +const defaultTransient = Object.freeze( { isReady: false, - isBusy: false, -}; + activities: new Map(), -const defaultPersistent = { + // Read only values, provided by the server via hydrate. + merchant: Object.freeze( { + isConnected: false, + isSandbox: false, + id: '', + email: '', + } ), + + wooSettings: Object.freeze( { + storeCountry: '', + storeCurrency: '', + } ), +} ); + +const defaultPersistent = Object.freeze( { useSandbox: false, useManualConnection: false, clientId: '', clientSecret: '', -}; +} ); // Reducer logic. @@ -38,8 +51,56 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, { [ ACTION_TYPES.SET_PERSISTENT ]: ( state, action ) => setPersistent( state, action ), - [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => - setPersistent( state, payload.data ), + [ ACTION_TYPES.RESET ]: ( state ) => { + const cleanState = setTransient( + setPersistent( state, defaultPersistent ), + defaultTransient + ); + + // Keep "read-only" details and initialization flags. + cleanState.wooSettings = { ...state.wooSettings }; + cleanState.isReady = true; + + return cleanState; + }, + + [ ACTION_TYPES.START_ACTIVITY ]: ( state, payload ) => { + return setTransient( state, { + activities: new Map( state.activities ).set( + payload.id, + payload.description + ), + } ); + }, + + [ ACTION_TYPES.STOP_ACTIVITY ]: ( state, payload ) => { + const newActivities = new Map( state.activities ); + newActivities.delete( payload.id ); + return setTransient( state, { activities: newActivities } ); + }, + + [ ACTION_TYPES.DO_REFRESH_MERCHANT ]: ( state ) => ( { + ...state, + merchant: Object.freeze( { ...defaultTransient.merchant } ), + } ), + + [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => { + const newState = setPersistent( state, payload.data ); + + // Populate read-only properties. + [ 'wooSettings', 'merchant' ].forEach( ( key ) => { + if ( ! payload[ key ] ) { + return; + } + + newState[ key ] = Object.freeze( { + ...newState[ key ], + ...payload[ key ], + } ); + } ); + + return newState; + }, } ); export default commonReducer; diff --git a/modules/ppcp-settings/resources/js/data/common/selectors.js b/modules/ppcp-settings/resources/js/data/common/selectors.js index 14334fcf3..fde5d8c9e 100644 --- a/modules/ppcp-settings/resources/js/data/common/selectors.js +++ b/modules/ppcp-settings/resources/js/data/common/selectors.js @@ -16,6 +16,20 @@ export const persistentData = ( state ) => { }; export const transientData = ( state ) => { - const { data, ...transientState } = getState( state ); + const { data, merchant, wooSettings, ...transientState } = + getState( state ); return transientState || EMPTY_OBJ; }; + +export const getActivityList = ( state ) => { + const { activities = new Map() } = state; + return Object.fromEntries( activities ); +}; + +export const merchant = ( state ) => { + return getState( state ).merchant || EMPTY_OBJ; +}; + +export const wooSettings = ( state ) => { + return getState( state ).wooSettings || EMPTY_OBJ; +}; diff --git a/modules/ppcp-settings/resources/js/data/debug.js b/modules/ppcp-settings/resources/js/data/debug.js index b292d1920..6380c6d6a 100644 --- a/modules/ppcp-settings/resources/js/data/debug.js +++ b/modules/ppcp-settings/resources/js/data/debug.js @@ -1,4 +1,4 @@ -import { OnboardingStoreName } from './index'; +import { OnboardingStoreName, CommonStoreName } from './index'; export const addDebugTools = ( context, modules ) => { if ( ! context || ! context?.debug ) { @@ -33,9 +33,14 @@ export const addDebugTools = ( context, modules ) => { }; context.resetStore = () => { - const onboarding = wp.data.dispatch( OnboardingStoreName ); - onboarding.reset(); - onboarding.persist(); + const stores = [ OnboardingStoreName, CommonStoreName ]; + + stores.forEach( ( storeName ) => { + const store = wp.data.dispatch( storeName ); + + store.reset(); + store.persist(); + } ); }; context.startOnboarding = () => { diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index 4ae5bd947..e8582821e 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -34,8 +34,12 @@ const useHooks = () => { setProducts, } = useDispatch( STORE_NAME ); - // Read-only flags. + // Read-only flags and derived state. const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] ); + const determineProducts = useSelect( + ( select ) => select( STORE_NAME ).determineProducts(), + [] + ); // Transient accessors. const isReady = useTransient( 'isReady' ); @@ -80,6 +84,7 @@ const useHooks = () => { ); return savePersistent( setProducts, validProducts ); }, + determineProducts, }; }; @@ -113,3 +118,24 @@ export const useSteps = () => { return { flags, isReady, step, setStep, completed, setCompleted }; }; + +export const useNavigationState = () => { + const products = useProducts(); + const business = useBusiness(); + + return { + products, + business, + }; +}; + +export const useDetermineProducts = () => { + const { determineProducts } = useHooks(); + + return determineProducts; +}; + +export const useFlags = () => { + const { flags } = useHooks(); + return flags; +}; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index 176d4875d..2b16e2416 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -12,24 +12,25 @@ import ACTION_TYPES from './action-types'; // Store structure. -const defaultTransient = { +const defaultTransient = Object.freeze( { isReady: false, // Read only values, provided by the server. - flags: { + flags: Object.freeze( { canUseCasualSelling: false, canUseVaulting: false, canUseCardPayments: false, - }, -}; + canUseSubscriptions: false, + } ), +} ); -const defaultPersistent = { +const defaultPersistent = Object.freeze( { completed: false, step: 0, isCasualSeller: null, // null value will uncheck both options in the UI. - areOptionalPaymentMethodsEnabled: true, + areOptionalPaymentMethodsEnabled: null, products: [], -}; +} ); // Reducer logic. @@ -45,15 +46,28 @@ const onboardingReducer = createReducer( defaultTransient, defaultPersistent, { [ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) => setPersistent( state, payload ), - [ ACTION_TYPES.RESET ]: ( state ) => - setPersistent( state, defaultPersistent ), + [ ACTION_TYPES.RESET ]: ( state ) => { + const cleanState = setTransient( + setPersistent( state, defaultPersistent ), + defaultTransient + ); + + // Keep "read-only" details and initialization flags. + cleanState.flags = { ...state.flags }; + cleanState.isReady = true; + + return cleanState; + }, [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => { const newState = setPersistent( state, payload.data ); // Flags are not updated by `setPersistent()`. if ( payload.flags ) { - newState.flags = { ...newState.flags, ...payload.flags }; + newState.flags = Object.freeze( { + ...newState.flags, + ...payload.flags, + } ); } return newState; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js index d4d57ef4d..2e0953437 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js @@ -23,3 +23,50 @@ export const transientData = ( state ) => { export const flags = ( state ) => { return getState( state ).flags || EMPTY_OBJ; }; + +/** + * Returns the products that we use for the production login link in the last onboarding step. + * + * This selector does not return state-values, but uses the state to derive the products-array + * that should be returned. + * + * @param {{}} state + * @return {string[]} The ISU products, based on choices made in the onboarding wizard. + */ +export const determineProducts = ( state ) => { + const derivedProducts = []; + + const { isCasualSeller, areOptionalPaymentMethodsEnabled } = + persistentData( state ); + const { canUseVaulting, canUseCardPayments } = flags( state ); + + if ( ! canUseCardPayments || ! areOptionalPaymentMethodsEnabled ) { + /** + * Branch 1: Credit Card Payments not available. + * The store uses the Express-checkout product. + */ + derivedProducts.push( 'EXPRESS_CHECKOUT' ); + } else if ( isCasualSeller ) { + /** + * Branch 2: Merchant has no business. + * The store uses the Express-checkout product. + */ + derivedProducts.push( 'EXPRESS_CHECKOUT' ); + + // TODO: Add the "BCDC" product/feature + // Requirement: "EXPRESS_CHECKOUT with BCDC" + } else { + /** + * Branch 3: Merchant is business, and can use CC payments. + * The store uses the advanced PPCP product. + */ + derivedProducts.push( 'PPCP' ); + } + + if ( canUseVaulting ) { + // TODO: Add the "Vaulting" product/feature + // Requirement: "... with Vault" + } + + return derivedProducts; +}; diff --git a/modules/ppcp-settings/resources/js/hooks/useAccordionState.js b/modules/ppcp-settings/resources/js/hooks/useAccordionState.js new file mode 100644 index 000000000..f54018262 --- /dev/null +++ b/modules/ppcp-settings/resources/js/hooks/useAccordionState.js @@ -0,0 +1,39 @@ +import { useEffect, useState } from '@wordpress/element'; + +const checkIfCurrentTab = ( id ) => { + return id && window.location.hash === `#${ id }`; +}; + +const determineInitialState = ( id, initiallyOpen ) => { + if ( initiallyOpen !== null ) { + return initiallyOpen; + } + return checkIfCurrentTab( id ); +}; + +export function useAccordionState( { id = '', initiallyOpen = null } ) { + const [ isOpen, setIsOpen ] = useState( + determineInitialState( id, initiallyOpen ) + ); + + useEffect( () => { + const handleHashChange = () => { + if ( checkIfCurrentTab( id ) ) { + setIsOpen( true ); + } + }; + + window.addEventListener( 'hashchange', handleHashChange ); + return () => { + window.removeEventListener( 'hashchange', handleHashChange ); + }; + }, [ id ] ); + + const toggleOpen = ( ev ) => { + setIsOpen( ! isOpen ); + ev?.preventDefault(); + return false; + }; + + return { isOpen, toggleOpen }; +} diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js new file mode 100644 index 000000000..d34e74f42 --- /dev/null +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -0,0 +1,214 @@ +import { __ } from '@wordpress/i18n'; +import { useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; + +import { CommonHooks, OnboardingHooks } from '../data'; +import { openPopup } from '../utils/window'; + +const MESSAGES = { + CONNECTED: __( 'Connected to PayPal', 'woocommerce-paypal-payments' ), + POPUP_BLOCKED: __( + 'Popup blocked. Please allow popups for this site to connect to PayPal.', + 'woocommerce-paypal-payments' + ), + SANDBOX_ERROR: __( + 'Could not generate a Sandbox login link.', + 'woocommerce-paypal-payments' + ), + PRODUCTION_ERROR: __( + 'Could not generate a login link.', + 'woocommerce-paypal-payments' + ), + MANUAL_ERROR: __( + 'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.', + 'woocommerce-paypal-payments' + ), + LOGIN_FAILED: __( + 'Login was not successful. Please try again.', + 'woocommerce-paypal-payments' + ), +}; + +const ACTIVITIES = { + CONNECT_SANDBOX: 'ISU_LOGIN_SANDBOX', + CONNECT_PRODUCTION: 'ISU_LOGIN_PRODUCTION', + CONNECT_MANUAL: 'MANUAL_LOGIN', +}; + +const handlePopupWithCompletion = ( url, onError ) => { + return new Promise( ( resolve ) => { + const popup = openPopup( url ); + + if ( ! popup ) { + onError( MESSAGES.POPUP_BLOCKED ); + resolve( false ); + return; + } + + // Check popup state every 500ms + const checkPopup = setInterval( () => { + if ( popup.closed ) { + clearInterval( checkPopup ); + resolve( true ); + } + }, 500 ); + + return () => { + clearInterval( checkPopup ); + + if ( popup && ! popup.closed ) { + popup.close(); + } + }; + } ); +}; + +const useConnectionBase = () => { + const { setCompleted } = OnboardingHooks.useSteps(); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + const { verifyLoginStatus } = CommonHooks.useMerchantInfo(); + + return { + handleFailed: ( res, genericMessage ) => { + console.error( 'Connection error', res ); + createErrorNotice( res?.message ?? genericMessage ); + }, + handleCompleted: async () => { + try { + const loginSuccessful = await verifyLoginStatus(); + + if ( loginSuccessful ) { + createSuccessNotice( MESSAGES.CONNECTED ); + await setCompleted( true ); + } else { + createErrorNotice( MESSAGES.LOGIN_FAILED ); + } + } catch ( error ) { + createErrorNotice( error.message ?? MESSAGES.LOGIN_FAILED ); + } + }, + createErrorNotice, + }; +}; + +const useConnectionAttempt = ( connectFn, errorMessage ) => { + const { handleFailed, createErrorNotice, handleCompleted } = + useConnectionBase(); + + return async ( ...args ) => { + const res = await connectFn( ...args ); + + if ( ! res.success || ! res.data ) { + handleFailed( res, errorMessage ); + return false; + } + + const popupClosed = await handlePopupWithCompletion( + res.data, + createErrorNotice + ); + + if ( popupClosed ) { + await handleCompleted(); + } + + return popupClosed; + }; +}; + +export const useSandboxConnection = () => { + const { connectToSandbox, isSandboxMode, setSandboxMode } = + CommonHooks.useSandbox(); + const { withActivity } = CommonHooks.useBusyState(); + const connectionAttempt = useConnectionAttempt( + connectToSandbox, + MESSAGES.SANDBOX_ERROR + ); + + const handleSandboxConnect = async () => { + return withActivity( + ACTIVITIES.CONNECT_SANDBOX, + 'Connecting to sandbox account', + connectionAttempt + ); + }; + + return { + handleSandboxConnect, + isSandboxMode, + setSandboxMode, + }; +}; + +export const useProductionConnection = () => { + const { connectToProduction } = CommonHooks.useProduction(); + const { withActivity } = CommonHooks.useBusyState(); + const products = OnboardingHooks.useDetermineProducts(); + const connectionAttempt = useConnectionAttempt( + () => connectToProduction( products ), + MESSAGES.PRODUCTION_ERROR + ); + + const handleProductionConnect = async () => { + return withActivity( + ACTIVITIES.CONNECT_PRODUCTION, + 'Connecting to production account', + connectionAttempt + ); + }; + + return { handleProductionConnect }; +}; + +export const useManualConnection = () => { + const { handleFailed, handleCompleted, createErrorNotice } = + useConnectionBase(); + const { withActivity } = CommonHooks.useBusyState(); + const { + connectViaIdAndSecret, + isManualConnectionMode, + setManualConnectionMode, + clientId, + setClientId, + clientSecret, + setClientSecret, + } = CommonHooks.useManualConnection(); + + const handleConnectViaIdAndSecret = async ( { validation } = {} ) => { + return withActivity( + ACTIVITIES.CONNECT_MANUAL, + 'Connecting manually via Client ID and Secret', + async () => { + if ( 'function' === typeof validation ) { + try { + validation(); + } catch ( exception ) { + createErrorNotice( exception.message ); + return; + } + } + + const res = await connectViaIdAndSecret(); + + if ( res.success ) { + await handleCompleted(); + } else { + handleFailed( res, MESSAGES.MANUAL_ERROR ); + } + + return res.success; + } + ); + }; + + return { + handleConnectViaIdAndSecret, + isManualConnectionMode, + setManualConnectionMode, + clientId, + setClientId, + clientSecret, + setClientSecret, + }; +}; diff --git a/modules/ppcp-settings/resources/js/hooks/useIsScrolled.js b/modules/ppcp-settings/resources/js/hooks/useIsScrolled.js new file mode 100644 index 000000000..2b40aa3e9 --- /dev/null +++ b/modules/ppcp-settings/resources/js/hooks/useIsScrolled.js @@ -0,0 +1,44 @@ +/** + * Taken from WooCommerce core: + * https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/client/admin/client/hooks/useIsScrolled.js + */ + +import { useEffect, useRef, useState } from '@wordpress/element'; + +const isAtBottom = () => + window.innerHeight + window.scrollY >= document.body.scrollHeight; + +const useIsScrolled = () => { + const [ isScrolled, setIsScrolled ] = useState( false ); + const [ atBottom, setAtBottom ] = useState( isAtBottom() ); + const rafHandle = useRef( null ); + useEffect( () => { + const updateIsScrolled = () => { + setIsScrolled( window.pageYOffset > 20 ); + setAtBottom( isAtBottom() ); + }; + + const scrollListener = () => { + rafHandle.current = + window.requestAnimationFrame( updateIsScrolled ); + }; + + window.addEventListener( 'scroll', scrollListener ); + + window.addEventListener( 'resize', scrollListener ); + + return () => { + window.removeEventListener( 'scroll', scrollListener ); + window.removeEventListener( 'resize', scrollListener ); + window.cancelAnimationFrame( rafHandle.current ); + }; + }, [] ); + + return { + isScrolled, + atBottom, + atTop: ! isScrolled, + }; +}; + +export default useIsScrolled; diff --git a/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js b/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js index 193efd584..34bfc8e7f 100644 --- a/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js +++ b/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js @@ -1,5 +1,5 @@ export const countryPriceInfo = { - us: { + US: { currencySymbol: '$', fixedFee: 0.49, checkout: 3.49, @@ -9,7 +9,7 @@ export const countryPriceInfo = { fastlane: 2.59, standardCardFields: 2.99, }, - uk: { + UK: { currencySymbol: '£', fixedFee: 0.3, checkout: 2.9, @@ -18,7 +18,7 @@ export const countryPriceInfo = { apm: 1.2, standardCardFields: 1.2, }, - ca: { + CA: { currencySymbol: '$', fixedFee: 0.3, checkout: 2.9, @@ -27,7 +27,7 @@ export const countryPriceInfo = { apm: 2.9, standardCardFields: 2.9, }, - au: { + AU: { currencySymbol: '$', fixedFee: 0.3, checkout: 2.6, @@ -36,7 +36,7 @@ export const countryPriceInfo = { apm: 2.6, standardCardFields: 2.6, }, - fr: { + FR: { currencySymbol: '€', fixedFee: 0.35, checkout: 2.9, @@ -45,7 +45,7 @@ export const countryPriceInfo = { apm: 1.2, standardCardFields: 1.2, }, - it: { + IT: { currencySymbol: '€', fixedFee: 0.35, checkout: 3.4, @@ -54,7 +54,7 @@ export const countryPriceInfo = { apm: 1.2, standardCardFields: 1.2, }, - de: { + DE: { currencySymbol: '€', fixedFee: 0.39, checkout: 2.99, @@ -63,7 +63,7 @@ export const countryPriceInfo = { apm: 2.99, standardCardFields: 2.99, }, - es: { + ES: { currencySymbol: '€', fixedFee: 0.35, checkout: 2.9, diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index d213aa4c0..c1eeca241 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -19,7 +19,9 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; +use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; return array( 'settings.url' => static function ( ContainerInterface $container ) : string { @@ -37,6 +39,8 @@ $can_use_casual_selling = $container->get( 'settings.casual-selling.eligible' ); $can_use_vaulting = $container->has( 'save-payment-methods.eligible' ) && $container->get( 'save-payment-methods.eligible' ); $can_use_card_payments = $container->has( 'card-fields.eligible' ) && $container->get( 'card-fields.eligible' ); + $can_use_subscriptions = $container->has( 'wc-subscriptions.helper' ) && $container->get( 'wc-subscriptions.helper' ) + ->plugin_is_active(); // Card payments are disabled for this plugin when WooPayments is active. // TODO: Move this condition to the card-fields.eligible service? @@ -47,14 +51,18 @@ return new OnboardingProfile( $can_use_casual_selling, $can_use_vaulting, - $can_use_card_payments + $can_use_card_payments, + $can_use_subscriptions ); }, 'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings { return new GeneralSettings(); }, 'settings.data.common' => static function ( ContainerInterface $container ) : CommonSettings { - return new CommonSettings(); + return new CommonSettings( + $container->get( 'api.shop.country' ), + $container->get( 'api.shop.currency.getter' )->get(), + ); }, 'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint { return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) ); @@ -131,9 +139,25 @@ return in_array( $country, $eligible_countries, true ); }, + 'settings.handler.connection-listener' => static function ( ContainerInterface $container ) : ConnectionListener { + $page_id = $container->has( 'wcgateway.current-ppcp-settings-page-id' ) ? $container->get( 'wcgateway.current-ppcp-settings-page-id' ) : ''; + + return new ConnectionListener( + $page_id, + $container->get( 'settings.data.common' ), + $container->get( 'settings.service.onboarding-url-manager' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, 'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache { return new Cache( 'ppcp-paypal-signup-link' ); }, + 'settings.service.onboarding-url-manager' => static function ( ContainerInterface $container ) : OnboardingUrlManager { + return new OnboardingUrlManager( + $container->get( 'settings.service.signup-link-cache' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, 'settings.service.connection-url-generators' => static function ( ContainerInterface $container ) : array { // Define available environments. $environments = array( @@ -152,8 +176,8 @@ $generators[ $environment ] = new ConnectionUrlGenerator( $config['partner_referrals'], $container->get( 'api.repository.partner-referrals-data' ), - $container->get( 'settings.service.signup-link-cache' ), $environment, + $container->get( 'settings.service.onboarding-url-manager' ), $container->get( 'woocommerce.logger.woocommerce' ) ); } diff --git a/modules/ppcp-settings/src/Data/CommonSettings.php b/modules/ppcp-settings/src/Data/CommonSettings.php index 8f7dd1ddf..1894255ff 100644 --- a/modules/ppcp-settings/src/Data/CommonSettings.php +++ b/modules/ppcp-settings/src/Data/CommonSettings.php @@ -29,6 +29,25 @@ class CommonSettings extends AbstractDataModel { */ protected const OPTION_KEY = 'woocommerce-ppcp-data-common'; + /** + * List of customization flags, provided by the server (read-only). + * + * @var array + */ + protected array $woo_settings = array(); + + /** + * Constructor. + * + * @param string $country WooCommerce store country. + * @param string $currency WooCommerce store currency. + */ + public function __construct( string $country, string $currency ) { + parent::__construct(); + $this->woo_settings['country'] = $country; + $this->woo_settings['currency'] = $currency; + } + /** * Get default values for the model. * @@ -40,6 +59,12 @@ protected function get_defaults() : array { 'use_manual_connection' => false, 'client_id' => '', 'client_secret' => '', + + // Details about connected merchant account. + 'merchant_connected' => false, + 'sandbox_merchant' => false, + 'merchant_id' => '', + 'merchant_email' => '', ); } @@ -116,4 +141,67 @@ public function get_client_secret() : string { public function set_client_secret( string $client_secret ) : void { $this->data['client_secret'] = sanitize_text_field( $client_secret ); } + + /** + * Returns the list of read-only customization flags. + * + * @return array + */ + public function get_woo_settings() : array { + return $this->woo_settings; + } + + /** + * Setter to update details of the connected merchant account. + * + * Those details cannot be changed individually. + * + * @param bool $is_sandbox Whether the details are for a sandbox account. + * @param string $merchant_id The merchant ID. + * @param string $merchant_email The merchant's email. + * + * @return void + */ + public function set_merchant_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void { + $this->data['sandbox_merchant'] = $is_sandbox; + $this->data['merchant_id'] = sanitize_text_field( $merchant_id ); + $this->data['merchant_email'] = sanitize_email( $merchant_email ); + $this->data['merchant_connected'] = true; + } + + /** + * Whether the currently connected merchant is a sandbox account. + * + * @return bool + */ + public function is_sandbox_merchant() : bool { + return $this->data['sandbox_merchant']; + } + + /** + * Whether the merchant successfully logged into their PayPal account. + * + * @return bool + */ + public function is_merchant_connected() : bool { + return $this->data['merchant_connected'] && $this->data['merchant_id'] && $this->data['merchant_email']; + } + + /** + * Gets the currently connected merchant ID. + * + * @return string + */ + public function get_merchant_id() : string { + return $this->data['merchant_id']; + } + + /** + * Gets the currently connected merchant's email. + * + * @return string + */ + public function get_merchant_email() : string { + return $this->data['merchant_email']; + } } diff --git a/modules/ppcp-settings/src/Data/OnboardingProfile.php b/modules/ppcp-settings/src/Data/OnboardingProfile.php index 03a0a7d1c..a2d8e6c36 100644 --- a/modules/ppcp-settings/src/Data/OnboardingProfile.php +++ b/modules/ppcp-settings/src/Data/OnboardingProfile.php @@ -42,19 +42,22 @@ class OnboardingProfile extends AbstractDataModel { * @param bool $can_use_casual_selling Whether casual selling is enabled in the store's country. * @param bool $can_use_vaulting Whether vaulting is enabled in the store's country. * @param bool $can_use_card_payments Whether credit card payments are possible. + * @param bool $can_use_subscriptions Whether WC Subscriptions plugin is active. * * @throws RuntimeException If the OPTION_KEY is not defined in the child class. */ public function __construct( bool $can_use_casual_selling = false, bool $can_use_vaulting = false, - bool $can_use_card_payments = false + bool $can_use_card_payments = false, + bool $can_use_subscriptions = false ) { parent::__construct(); $this->flags['can_use_casual_selling'] = $can_use_casual_selling; $this->flags['can_use_vaulting'] = $can_use_vaulting; $this->flags['can_use_card_payments'] = $can_use_card_payments; + $this->flags['can_use_subscriptions'] = $can_use_subscriptions; } /** @@ -67,7 +70,7 @@ protected function get_defaults() : array { 'completed' => false, 'step' => 0, 'is_casual_seller' => null, - 'are_optional_payment_methods_enabled' => true, + 'are_optional_payment_methods_enabled' => null, 'products' => array(), ); } diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index c7345148e..3c0131759 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -60,6 +60,40 @@ class CommonRestEndpoint extends RestEndpoint { ), ); + /** + * Map merchant details to JS names. + * + * @var array + */ + private array $merchant_info_map = array( + 'merchant_connected' => array( + 'js_name' => 'isConnected', + ), + 'sandbox_merchant' => array( + 'js_name' => 'isSandbox', + ), + 'merchant_id' => array( + 'js_name' => 'id', + ), + 'merchant_email' => array( + 'js_name' => 'email', + ), + ); + + /** + * Map woo-settings to JS names. + * + * @var array + */ + private array $woo_settings_map = array( + 'country' => array( + 'js_name' => 'storeCountry', + ), + 'currency' => array( + 'js_name' => 'storeCurrency', + ), + ); + /** * Constructor. * @@ -96,6 +130,18 @@ public function register_routes() { ), ) ); + + register_rest_route( + $this->namespace, + "/$this->rest_base/merchant", + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_merchant_details' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + ) + ); } /** @@ -109,7 +155,10 @@ public function get_details() : WP_REST_Response { $this->field_map ); - return $this->return_success( $js_data ); + $extra_data = $this->add_woo_settings( array() ); + $extra_data = $this->add_merchant_info( $extra_data ); + + return $this->return_success( $js_data, $extra_data ); } /** @@ -130,4 +179,50 @@ public function update_details( WP_REST_Request $request ) : WP_REST_Response { return $this->get_details(); } + + /** + * Returns only the (read-only) merchant details from the DB. + * + * @return WP_REST_Response Merchant details. + */ + public function get_merchant_details() : WP_REST_Response { + $js_data = array(); // No persistent data. + $extra_data = $this->add_merchant_info( array() ); + + return $this->return_success( $js_data, $extra_data ); + } + + /** + * Appends the "merchant" attribute to the extra_data collection, which + * contains details about the merchant's PayPal account, like the merchant ID. + * + * @param array $extra_data Initial extra_data collection. + * + * @return array Updated extra_data collection. + */ + protected function add_merchant_info( array $extra_data ) : array { + $extra_data['merchant'] = $this->sanitize_for_javascript( + $this->settings->to_array(), + $this->merchant_info_map + ); + + return $extra_data; + } + + /** + * Appends the "wooSettings" attribute to the extra_data collection to + * provide WooCommerce store details, like the store country and currency. + * + * @param array $extra_data Initial extra_data collection. + * + * @return array Updated extra_data collection. + */ + protected function add_woo_settings( array $extra_data ) : array { + $extra_data['wooSettings'] = $this->sanitize_for_javascript( + $this->settings->get_woo_settings(), + $this->woo_settings_map + ); + + return $extra_data; + } } diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index 02e7c80cd..d4273228f 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -77,6 +77,9 @@ class OnboardingRestEndpoint extends RestEndpoint { 'can_use_card_payments' => array( 'js_name' => 'canUseCardPayments', ), + 'can_use_subscriptions' => array( + 'js_name' => 'canUseSubscriptions', + ), ); /** diff --git a/modules/ppcp-settings/src/Handler/ConnectionListener.php b/modules/ppcp-settings/src/Handler/ConnectionListener.php new file mode 100644 index 000000000..a24a82231 --- /dev/null +++ b/modules/ppcp-settings/src/Handler/ConnectionListener.php @@ -0,0 +1,241 @@ +settings_page_id = $settings_page_id; + $this->settings = $settings; + $this->url_manager = $url_manager; + $this->logger = $logger ?: new NullLogger(); + + // Initialize as "guest", the real ID is provided via process(). + $this->user_id = 0; + } + + /** + * Process the request data, and extract connection details, if present. + * + * @param int $user_id The current user ID. + * @param array $request Request details to process. + */ + public function process( int $user_id, array $request ) : void { + $this->user_id = $user_id; + + if ( ! $this->is_valid_request( $request ) ) { + return; + } + + $token = $this->get_token_from_request( $request ); + if ( ! $this->url_manager->validate_token_and_delete( $token, $this->user_id ) ) { + return; + } + + $data = $this->extract_data( $request ); + if ( ! $data ) { + return; + } + + $this->logger->info( 'Found merchant data in request', $data ); + + $this->store_data( + $data['is_sandbox'], + $data['merchant_id'], + $data['merchant_email'] + ); + } + + /** + * Determine, if the request details contain connection data that should be + * extracted and stored. + * + * @param array $request Request details to verify. + * + * @return bool True, if the request contains valid connection details. + */ + protected function is_valid_request( array $request ) : bool { + if ( $this->user_id < 1 || ! $this->settings_page_id ) { + return false; + } + + if ( ! user_can( $this->user_id, 'manage_woocommerce' ) ) { + return false; + } + + $required_params = array( + 'merchantIdInPayPal', + 'merchantId', + 'ppcpToken', + ); + + foreach ( $required_params as $param ) { + if ( empty( $request[ $param ] ) ) { + return false; + } + } + + return true; + } + + /** + * Extract the merchant details (ID & email) from the request details. + * + * @param array $request The full request details. + * + * @return array Structured array with 'is_sandbox', 'merchant_id', and 'merchant_email' keys, + * or an empty array on failure. + */ + protected function extract_data( array $request ) : array { + $this->logger->info( 'Extracting connection data from request...' ); + + $merchant_id = $this->get_merchant_id_from_request( $request ); + $merchant_email = $this->get_merchant_email_from_request( $request ); + + if ( ! $merchant_id || ! $merchant_email ) { + return array(); + } + + return array( + 'is_sandbox' => $this->settings->get_sandbox(), + 'merchant_id' => $merchant_id, + 'merchant_email' => $merchant_email, + ); + } + + /** + * Persist the merchant details to the database. + * + * @param bool $is_sandbox Whether the details are for a sandbox account. + * @param string $merchant_id The anonymized merchant ID. + * @param string $merchant_email The merchant's email. + */ + protected function store_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void { + $this->logger->info( "Save merchant details to the DB: $merchant_email ($merchant_id)" ); + + $this->settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email ); + $this->settings->save(); + } + + /** + * Returns the sanitized connection token from the incoming request. + * + * @param array $request Full request details. + * + * @return string The sanitized token, or an empty string. + */ + protected function get_token_from_request( array $request ) : string { + return $this->sanitize_string( $request['ppcpToken'] ?? '' ); + } + + /** + * Returns the sanitized merchant ID from the incoming request. + * + * @param array $request Full request details. + * + * @return string The sanitized merchant ID, or an empty string. + */ + protected function get_merchant_id_from_request( array $request ) : string { + return $this->sanitize_string( $request['merchantIdInPayPal'] ?? '' ); + } + + /** + * Returns the sanitized merchant email from the incoming request. + * + * Note that the email is provided via the argument "merchantId", which + * looks incorrect at first, but PayPal uses the email address as merchant + * IDm and offers a more anonymous ID via the "merchantIdInPayPal" argument. + * + * @param array $request Full request details. + * + * @return string The sanitized merchant email, or an empty string. + */ + protected function get_merchant_email_from_request( array $request ) : string { + return $this->sanitize_merchant_email( $request['merchantId'] ?? '' ); + } + + /** + * Sanitizes a request-argument for processing. + * + * @param string $value Value from the request argument. + * + * @return string Sanitized value. + */ + protected function sanitize_string( string $value ) : string { + return trim( sanitize_text_field( wp_unslash( $value ) ) ); + } + + /** + * Sanitizes the merchant's email address for processing. + * + * @param string $email The plain email. + * + * @return string Sanitized email address. + */ + protected function sanitize_merchant_email( string $email ) : string { + return sanitize_text_field( str_replace( ' ', '+', $email ) ); + } +} diff --git a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php index 6e91aba3a..028740cb9 100644 --- a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php +++ b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php @@ -14,9 +14,11 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; -use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl; use WooCommerce\WooCommerce\Logging\Logger\NullLogger; +// TODO: Replace the OnboardingUrl with a new implementation for this module. +use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl; + /** * Generator that builds the ISU connection URL. */ @@ -36,11 +38,11 @@ class ConnectionUrlGenerator { protected PartnerReferralsData $referrals_data; /** - * The cache + * Manages access to OnboardingUrl instances * - * @var Cache + * @var OnboardingUrlManager */ - protected Cache $cache; + protected OnboardingUrlManager $url_manager; /** * Which environment is used for the connection URL. @@ -54,7 +56,7 @@ class ConnectionUrlGenerator { * * @var LoggerInterface */ - private $logger; + private LoggerInterface $logger; /** * Constructor for the ConnectionUrlGenerator class. @@ -63,23 +65,22 @@ class ConnectionUrlGenerator { * * @param PartnerReferrals $partner_referrals PartnerReferrals for URL generation. * @param PartnerReferralsData $referrals_data Default partner referrals data. - * @param Cache $cache The cache object used for storing and - * retrieving data. * @param string $environment Environment that is used to generate the URL. * ['production'|'sandbox']. + * @param OnboardingUrlManager $url_manager Manages access to OnboardingUrl instances. * @param ?LoggerInterface $logger The logger object for logging messages. */ public function __construct( PartnerReferrals $partner_referrals, PartnerReferralsData $referrals_data, - Cache $cache, string $environment, + OnboardingUrlManager $url_manager, ?LoggerInterface $logger = null ) { $this->partner_referrals = $partner_referrals; $this->referrals_data = $referrals_data; - $this->cache = $cache; $this->environment = $environment; + $this->url_manager = $url_manager; $this->logger = $logger ?: new NullLogger(); } @@ -107,7 +108,7 @@ public function environment() : string { public function generate( array $products = array() ) : string { $cache_key = $this->cache_key( $products ); $user_id = get_current_user_id(); - $onboarding_url = new OnboardingUrl( $this->cache, $cache_key, $user_id ); + $onboarding_url = $this->url_manager->get( $cache_key, $user_id ); $cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key ); if ( $cached_url ) { diff --git a/modules/ppcp-settings/src/Service/OnboardingUrlManager.php b/modules/ppcp-settings/src/Service/OnboardingUrlManager.php new file mode 100644 index 000000000..f2463af46 --- /dev/null +++ b/modules/ppcp-settings/src/Service/OnboardingUrlManager.php @@ -0,0 +1,101 @@ +cache = $cache; + $this->logger = $logger ?: new NullLogger(); + } + + /** + * Returns a new Onboarding Url instance. + * + * @param string $cache_key_prefix The prefix for the cache entry. + * @param int $user_id User ID to associate the link with. + * + * @return OnboardingUrl + */ + public function get( string $cache_key_prefix, int $user_id ) : OnboardingUrl { + return new OnboardingUrl( $this->cache, $cache_key_prefix, $user_id ); + } + + /** + * Validates the authentication token; if it's valid, the token is instantly + * invalidated (deleted), so it cannot be validated again. + * + * @param string $token The token to validate. + * @param int $user_id User ID who generated the token. + * + * @return bool True, if the token is valid. False otherwise. + */ + public function validate_token_and_delete( string $token, int $user_id ) : bool { + if ( $user_id < 1 || strlen( $token ) < 10 ) { + return false; + } + + $log_token = ( (string) substr( $token, 0, 2 ) ) . '...' . ( (string) substr( $token, - 6 ) ); + $this->logger->debug( 'Validating onboarding ppcpToken: ' . $log_token ); + + if ( OnboardingUrl::validate_token_and_delete( $this->cache, $token, $user_id ) ) { + $this->logger->info( 'Validated onboarding ppcpToken: ' . $log_token ); + + return true; + } + + if ( OnboardingUrl::validate_previous_token( $this->cache, $token, $user_id ) ) { + // TODO: Do we need this here? Previous logic was to reload the page without doing anything in this case. + $this->logger->info( 'Validated previous token, silently redirecting: ' . $log_token ); + + return true; + } + + $this->logger->error( 'Failed to validate onboarding ppcpToken: ' . $log_token ); + + return false; + } +} diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 7c9dca2f8..7cb55bb02 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -11,6 +11,7 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint; +use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; @@ -85,7 +86,7 @@ static function () use ( $container ) { } ); - $endpoint = $container->get( 'settings.switch-ui.endpoint' ); + $endpoint = $container->get( 'settings.switch-ui.endpoint' ) ? $container->get( 'settings.switch-ui.endpoint' ) : null; assert( $endpoint instanceof SwitchSettingsUiEndpoint ); add_action( @@ -189,6 +190,17 @@ static function () use ( $container ) : void { } ); + add_action( + 'admin_init', + static function () use ( $container ) : void { + $connection_handler = $container->get( 'settings.handler.connection-listener' ); + assert( $connection_handler instanceof ConnectionListener ); + + // @phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no nonce; sanitation done by the handler + $connection_handler->process( get_current_user_id(), $_GET ); + } + ); + return true; } diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index fbd511e93..8c807474e 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -653,7 +653,10 @@ static function () use ( $container ) { $listener = $container->get( 'wcgateway.settings.listener' ); assert( $listener instanceof SettingsListener ); - $listener->listen_for_merchant_id(); + $use_new_ui = $container->get( 'wcgateway.settings.admin-settings-enabled' ); + if ( ! $use_new_ui ) { + $listener->listen_for_merchant_id(); + } try { $listener->listen_for_vaulting_enabled();