diff --git a/modules/ppcp-onboarding/services.php b/modules/ppcp-onboarding/services.php
index aca3bed0a..2a6de82d8 100644
--- a/modules/ppcp-onboarding/services.php
+++ b/modules/ppcp-onboarding/services.php
@@ -22,7 +22,7 @@ use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingSendOnlyNoticeRendere
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer;
return array(
- 'api.sandbox-host' => static function ( ContainerInterface $container ): string {
+ 'api.sandbox-host' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
@@ -36,7 +36,7 @@ return array(
}
return CONNECT_WOO_SANDBOX_URL;
},
- 'api.production-host' => static function ( ContainerInterface $container ): string {
+ 'api.production-host' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
@@ -51,7 +51,7 @@ return array(
}
return CONNECT_WOO_URL;
},
- 'api.host' => static function ( ContainerInterface $container ): string {
+ 'api.host' => static function ( ContainerInterface $container ): string {
$environment = $container->get( 'onboarding.environment' );
/**
@@ -63,7 +63,7 @@ return array(
? (string) $container->get( 'api.sandbox-host' ) : (string) $container->get( 'api.production-host' );
},
- 'api.paypal-host' => function( ContainerInterface $container ) : string {
+ 'api.paypal-host' => function( ContainerInterface $container ) : string {
$environment = $container->get( 'onboarding.environment' );
/**
* The current environment.
@@ -76,7 +76,7 @@ return array(
return $container->get( 'api.paypal-host-production' );
},
- 'api.paypal-website-url' => function( ContainerInterface $container ) : string {
+ 'api.paypal-website-url' => function( ContainerInterface $container ) : string {
$environment = $container->get( 'onboarding.environment' );
assert( $environment instanceof Environment );
if ( $environment->current_environment_is( Environment::SANDBOX ) ) {
@@ -86,7 +86,7 @@ return array(
},
- 'api.bearer' => static function ( ContainerInterface $container ): Bearer {
+ 'api.bearer' => static function ( ContainerInterface $container ): Bearer {
$state = $container->get( 'onboarding.state' );
@@ -113,16 +113,16 @@ return array(
$settings
);
},
- 'onboarding.state' => function( ContainerInterface $container ) : State {
+ 'onboarding.state' => function( ContainerInterface $container ) : State {
$settings = $container->get( 'wcgateway.settings' );
return new State( $settings );
},
- 'onboarding.environment' => function( ContainerInterface $container ) : Environment {
+ 'onboarding.environment' => function( ContainerInterface $container ) : Environment {
$settings = $container->get( 'wcgateway.settings' );
return new Environment( $settings );
},
- 'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets {
+ 'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets {
$state = $container->get( 'onboarding.state' );
$login_seller_endpoint = $container->get( 'onboarding.endpoint.login-seller' );
return new OnboardingAssets(
@@ -135,14 +135,14 @@ return array(
);
},
- 'onboarding.url' => static function ( ContainerInterface $container ): string {
+ 'onboarding.url' => static function ( ContainerInterface $container ): string {
return plugins_url(
'/modules/ppcp-onboarding/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
- 'onboarding.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSellerEndpoint {
+ 'onboarding.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSellerEndpoint {
$request_data = $container->get( 'button.request-data' );
$login_seller_production = $container->get( 'api.endpoint.login-seller-production' );
@@ -162,7 +162,7 @@ return array(
new Cache( 'ppcp-client-credentials-cache' )
);
},
- 'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : UpdateSignupLinksEndpoint {
+ 'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : UpdateSignupLinksEndpoint {
return new UpdateSignupLinksEndpoint(
$container->get( 'wcgateway.settings' ),
$container->get( 'button.request-data' ),
@@ -172,10 +172,10 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
- 'onboarding.signup-link-cache' => static function( ContainerInterface $container ): Cache {
+ 'onboarding.signup-link-cache' => static function( ContainerInterface $container ): Cache {
return new Cache( 'ppcp-paypal-signup-link' );
},
- 'onboarding.signup-link-ids' => static function ( ContainerInterface $container ): array {
+ 'onboarding.signup-link-ids' => static function ( ContainerInterface $container ): array {
return array(
'production-ppcp',
'production-express_checkout',
@@ -183,12 +183,12 @@ return array(
'sandbox-express_checkout',
);
},
- 'onboarding.render-send-only-notice' => static function( ContainerInterface $container ) {
+ 'onboarding.render-send-only-notice' => static function( ContainerInterface $container ) {
return new OnboardingSendOnlyNoticeRenderer(
$container->get( 'wcgateway.send-only-message' )
);
},
- 'onboarding.render' => static function ( ContainerInterface $container ) : OnboardingRenderer {
+ 'onboarding.render' => static function ( ContainerInterface $container ) : OnboardingRenderer {
$partner_referrals = $container->get( 'api.endpoint.partner-referrals-production' );
$partner_referrals_sandbox = $container->get( 'api.endpoint.partner-referrals-sandbox' );
$partner_referrals_data = $container->get( 'api.repository.partner-referrals-data' );
@@ -204,14 +204,14 @@ return array(
$logger
);
},
- 'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer {
+ 'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer {
return new OnboardingOptionsRenderer(
$container->get( 'onboarding.url' ),
$container->get( 'api.shop.country' ),
$container->get( 'wcgateway.settings' )
);
},
- 'onboarding.rest' => static function( $container ) : OnboardingRESTController {
+ 'onboarding.rest' => static function( $container ) : OnboardingRESTController {
return new OnboardingRESTController( $container );
},
);
diff --git a/modules/ppcp-settings/resources/css/_mixins.scss b/modules/ppcp-settings/resources/css/_mixins.scss
index d2fac6cd2..8c83eb436 100644
--- a/modules/ppcp-settings/resources/css/_mixins.scss
+++ b/modules/ppcp-settings/resources/css/_mixins.scss
@@ -43,3 +43,14 @@
display: flex;
gap: $gap;
}
+
+@mixin disabled-state($control-type) {
+ .components-#{$control-type}-control.is-disabled {
+ .components-#{$control-type}-control__input,
+ .components-#{$control-type}-control__label,
+ .components-base-control__help {
+ opacity: 0.3;
+ cursor: default;
+ }
+ }
+}
diff --git a/modules/ppcp-settings/resources/css/_variables.scss b/modules/ppcp-settings/resources/css/_variables.scss
index 5ccf191e5..53483adb6 100644
--- a/modules/ppcp-settings/resources/css/_variables.scss
+++ b/modules/ppcp-settings/resources/css/_variables.scss
@@ -55,6 +55,8 @@ $card-vertical-gap: 48px;
--color-gray-100: #{$color-gray-100};
--color-gradient-dark: #{$color-gradient-dark};
+ --color-preview-background: #FAF8F5;
+ --color-separators: #{$color-gray-200};
--color-text-title: #{$color-gray-900};
--color-text-main: #{$color-text-text};
--color-text-teriary: #{$color-text-tertiary};
diff --git a/modules/ppcp-settings/resources/css/components/_reusable.scss b/modules/ppcp-settings/resources/css/components/_reusable.scss
index 4d4f5c1ba..2de887f57 100644
--- a/modules/ppcp-settings/resources/css/components/_reusable.scss
+++ b/modules/ppcp-settings/resources/css/components/_reusable.scss
@@ -4,6 +4,7 @@
@import './reusable-components/busy-state';
@import './reusable-components/button';
@import './reusable-components/fields';
+@import './reusable-components/hstack';
@import './reusable-components/navigation';
@import './reusable-components/onboarding-header';
@import './reusable-components/payment-method-icons';
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 1a9cf102c..195367dfb 100644
--- a/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss
+++ b/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss
@@ -1,27 +1,96 @@
-.ppcp-r {
+.ppcp-r__radio-value {
+ @include hide-input-field;
- &__radio-value {
- @include hide-input-field;
+ &:checked + .ppcp-r__radio-presentation {
+ position: relative;
- &:checked + .ppcp-r__radio-presentation {
- position: relative;
+ &::before {
+ content: '';
+ width: 12px;
+ height: 12px;
+ border-radius: 12px;
+ background-color: $color-blueberry;
+ display: block;
+ position: absolute;
+ transform: translate(-50%, -50%);
+ left: 50%;
+ top: 50%;
+ }
+ }
+}
- &::before {
- content: '';
- width: 12px;
- height: 12px;
- border-radius: 12px;
- background-color: $color-blueberry;
- display: block;
- position: absolute;
- transform: translate(-50%, -50%);
- left: 50%;
- top: 50%;
- }
+.ppcp-r__radio-presentation {
+ @include fake-input-field(20px);
+}
+
+.ppcp-r__checkbox-presentation {
+ @include fake-input-field(2px);
+}
+
+.ppcp-r__radio-wrapper {
+ display: flex;
+ gap: 18px;
+ align-items: center;
+ position: relative;
+
+ label {
+ @include font(13, 20, 400);
+ color: $color-gray-800;
+ }
+}
+
+.ppcp-r__radio-description {
+ @include font(13, 20, 400);
+ margin: 0;
+ color: $color-gray-800;
+}
+
+.ppcp-r__radio-content-additional {
+ padding-left: 38px;
+ padding-top: 18px;
+}
+
+
+.ppcp-r-app {
+ @include disabled-state('base');
+ @include disabled-state('checkbox');
+
+ .components-base-control__label {
+ @include font(13, 16, 600);
+ color: $color-gray-900;
+ text-transform: none;
+ }
+
+ .components-base-control__input {
+ border: 1px solid $color-gray-700;
+ border-radius: 2px;
+ box-shadow: none;
+
+ &:focus {
+ border-color: $color-blueberry;
}
}
- &__checkbox {
+ .components-base-control__help {
+ margin-bottom: 0;
+ }
+
+ // Text input fields.
+ input[type='text'] {
+ @include font(14, 20, 400);
+ @include primaryFont;
+ padding: 7px 11px;
+ border-radius: 2px;
+ }
+
+ // Select lists.
+ select {
+ @include font(14, 20, 400);
+ padding: 7px 27px 7px 11px;
+ }
+
+ // Checkboxes.
+ .components-checkbox-control {
position: relative;
input {
@@ -30,7 +99,7 @@
&:checked {
background-color: $color-blueberry;
- border-color:$color-blueberry;
+ border-color: $color-blueberry;
}
}
@@ -43,78 +112,17 @@
}
}
- &__radio-presentation {
- @include fake-input-field(20px);
+ // Custom styles.
+ .components-form-toggle.is-checked > .components-form-toggle__track {
+ background-color: $color-blueberry;
}
- &__checkbox-presentation {
- @include fake-input-field(2px);
- }
-
- &__radio-wrapper {
- display: flex;
- gap: 18px;
- align-items: center;
- position: relative;
-
- label {
- @include font(13, 20, 400);
- color: $color-gray-800;
- }
- }
-
- &__radio-description {
- @include font(13, 20, 400);
- margin: 0;
- color: $color-gray-800;
- }
-
- &__radio-content-additional {
- padding-left: 38px;
- padding-top: 18px;
- }
-}
-
-.components-base-control {
- &__label {
- color: $color-gray-900;
- @include font(13, 16, 600);
- text-transform: none;
- }
-
- &__input {
- border: 1px solid $color-gray-700;
- border-radius: 2px;
- box-shadow: none;
-
- &:focus {
- border-color: $color-blueberry;
+ .ppcp-r-vertical-text-control {
+ .components-base-control__field {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ margin: 0;
}
}
}
-
-
-input[type='text'] {
- padding: 7px 11px;
- @include font(14, 20, 400);
- @include primaryFont;
- border-radius: 2px;
-}
-
-select {
- padding: 7px 27px 7px 11px;
- @include font(14, 20, 400);
-}
-
-.components-form-toggle.is-checked > .components-form-toggle__track {
- background-color: $color-blueberry;
-}
-
-.ppcp-r-vertical-text-control {
- .components-base-control__field {
- display: flex;
- flex-direction: column;
- gap: 0;
- margin: 0;
- }
-}
diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_hstack.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_hstack.scss
new file mode 100644
index 000000000..e55584d1c
--- /dev/null
+++ b/modules/ppcp-settings/resources/css/components/reusable-components/_hstack.scss
@@ -0,0 +1,20 @@
+.components-flex {
+ display: flex;
+ -webkit-box-align: stretch;
+ align-items: stretch;
+ flex-direction: column;
+ -webkit-box-pack: center;
+ justify-content: center;
+
+ .components-h-stack {
+ flex-direction: row;
+ justify-content: flex-start;
+ gap: 32px;
+ }
+
+ // Fix layout for checkboxes inside a flex-stack.
+ .components-checkbox-control >.components-base-control__field > .components-flex {
+ flex-direction: row;
+ gap: 12px;
+ }
+}
diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss
index f65c5c9d5..8888bf2c0 100644
--- a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss
+++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss
@@ -4,7 +4,7 @@
.ppcp-r-settings-block {
display: flex;
flex-direction: column;
- gap: 16px 0;
+ gap: var(--block-item-gap, 16px);
&.ppcp-r-settings-block__input,
&.ppcp-r-settings-block__select {
@@ -16,8 +16,8 @@
flex-direction: column;
gap: 6px;
- &:not(:last-child):not(.ppcp-r-settings-block--accordion__header) {
- padding-bottom: 6px;
+ &:not(:last-child) {
+ padding-bottom: var(--block-header-gap, 6px);
}
}
@@ -27,6 +27,15 @@
display: block;
text-transform: uppercase;
+ &.style-alt {
+ @include font(14, 16, 600);
+ text-transform: none;
+ }
+
+ &.style-big {
+ @include font(16, 20, 600);
+ }
+
.ppcp-r-title-badge {
text-transform: none;
margin-left: 6px;
@@ -88,9 +97,18 @@
margin-left: 5px;
}
+ .ppcp-r-settings-block__action {
+ display: flex;
+ align-items: center;
+
+ .components-flex {
+ row-gap: 0;
+ }
+ }
+
+ .ppcp-r-settings-block:not(.no-gap) {
- margin-top: 32px;
- padding-top: 32px;
+ margin-top: var(--block-separator-gap, 32px);
+ padding-top: var(--block-separator-gap, 32px);
border-top: 1px solid var(--color-gray-200);
}
diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings.scss b/modules/ppcp-settings/resources/css/components/screens/_settings.scss
index 2f536ac96..0b8802138 100644
--- a/modules/ppcp-settings/resources/css/components/screens/_settings.scss
+++ b/modules/ppcp-settings/resources/css/components/screens/_settings.scss
@@ -1,4 +1,5 @@
@import './settings/input';
+@import './settings/connection-status';
@import './settings/tab-styling';
@import './settings/tab-paylater-configurator';
@@ -22,6 +23,7 @@
&:hover {
cursor: pointer;
+
.ppcp-r-todo-item__inner {
.ppcp-r-todo-item__description {
color: $color-text-text;
@@ -117,91 +119,7 @@
font-weight: 500;
}
- margin-top:24px;
- }
-}
-
-// 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;
- }
- }
+ margin-top: 24px;
}
}
diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_connection-status.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_connection-status.scss
new file mode 100644
index 000000000..2bf1417e6
--- /dev/null
+++ b/modules/ppcp-settings/resources/css/components/screens/settings/_connection-status.scss
@@ -0,0 +1,78 @@
+// 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;
+ }
+ }
+
+ &__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;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__data {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ width: 100%;
+ }
+
+ &__status-toggle--toggled {
+ .ppcp-r-connection-status__show-all-data {
+ transform: rotate(180deg);
+ }
+ }
+
+ &__status-row {
+ display: flex;
+ flex-direction: column;
+
+ 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;
+ }
+ }
+
+ @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;
+ }
+ }
+ }
+}
diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss
index ae49bed82..9444f4e18 100644
--- a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss
+++ b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss
@@ -1,6 +1,6 @@
.ppcp-r-paylater-configurator {
display: flex;
- border: 1px solid $color-gray-200;
+ border: 1px solid var(--color-separators);
border-radius: 8px;
overflow: hidden;
diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss
index c75d6a4c9..8645e7dad 100644
--- a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss
+++ b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss
@@ -1,121 +1,96 @@
.ppcp-r-styling {
+ --block-item-gap: 0;
+ --block-separator-gap: 24px;
+ --block-header-gap: 18px;
+ --panel-width: 422px;
+ --sticky-offset-top: 92px; // 32px admin-bar + 60px TopNavigation height
+ --preview-height-reduction: 236px; // 32px admin-bar + 60px TopNavigation height + 48px TopNavigation margin + 48px TabList height + 48px TabList margin
+
display: flex;
- border: 1px solid $color-gray-200;
+ border: 1px solid var(--color-separators);
border-radius: 8px;
- overflow: hidden;
- &__section:not(:last-child) {
- border-bottom: 1px solid black;
- padding-bottom: 24px;
- margin-bottom: 28px;
- border-bottom: 1px solid $color-gray-600;
+ .ppcp-r-settings-block {
+ &.header-section {
+ margin-bottom: 6px
+ }
+
+ &.location-selector {
+ position: sticky;
+ top: var(--sticky-offset-top);
+ background: var(--ppcp-color-app-bg);
+ z-index: 5;
+ padding: 16px 10px 8px;
+ margin: 0 -10px -8px;
+
+ .section-content {
+ display: flex;
+
+ & > .components-base-control:first-of-type {
+ width: 100%;
+ }
+ }
+ }
+
+ // Select-fields have a smaller gap between the header and input field.
+ &.has-select {
+ --block-header-gap: 8px;
+ }
+
+ // Above the payment methods is a slightly larger gap.
+ &.payment-methods {
+ --block-separator-gap: 28px;
+ }
+
+ // It has no header; adjusts the gap to the control right above the tagline.
+ &.tagline {
+ --block-header-gap: 24px;
+ }
}
- &__main-title {
- @include font(14, 20, 600);
- color: $color-gray-800;
- margin: 0 0 8px 0;
- display: block;
- }
-
- &__description {
- @include font(13, 20, 400);
- color: $color-gray-800;
- margin: 0 0 18px 0;
- }
-
- &__settings {
- width: 422px;
- background-color: $color-white;
+ /* The settings-panel (left side) */
+ .settings-panel {
+ width: var(--panel-width);
padding: 48px;
+
+ .ppcp-r-styling__section {
+ padding-bottom: 24px;
+ margin-bottom: 28px;
+ border-bottom: 1px solid var(--color-separators);
+
+ &.no-gap,
+ &:last-child {
+ padding-bottom: 0;
+ margin-bottom: 0;
+ border-bottom-style: none;
+ }
+ }
+
+ // Horizontal radio buttons have a width of 100px.
+ .components-radio-control__option {
+ min-width: 100px;
+ }
}
- &__preview {
- width: calc(100% - 422px);
- background-color: #FAF8F5;
- display: flex;
- align-items: center;
+ /* The preview area (right side) */
+ .preview-panel {
+ width: calc(100% - var(--panel-width));
+ background-color: var(--color-preview-background);
+ z-index: 0;
- &-inner {
+ .preview-panel-inner {
+ position: sticky;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
width: 100%;
padding: 24px;
- }
- }
+ height: calc(100vh - var(--preview-height-reduction));
+ top: var(--sticky-offset-top);
- &__section--rc {
- .ppcp-r-styling__title {
- @include font(13, 20, 600);
- color: $color-black;
- display: block;
- margin: 0 0 18px 0;
- }
- }
-
- &__section--empty.ppcp-r-styling__section {
- padding-bottom: 0;
- margin-bottom: 0;
- border-bottom: none;
- }
-
- &__select {
- label {
- @include font(13, 16, 600);
- color: $color-black;
- margin: 0;
- text-transform: none;
- }
-
- select {
- @include font(13, 20, 400);
- }
- }
-
- .ppcp-r__checkbox {
- .components-checkbox-control {
- &__label {
- @include font(13, 20, 400);
- color: $color-black;
- }
- }
-
- .components-flex {
- gap: 12px;
- }
- }
-
- &__payment-method-checkboxes {
- display: flex;
- flex-direction: column;
- gap: 24px;
- }
-}
-
-.ppcp-r {
- &__horizontal-control {
- .components-flex {
- flex-direction: row;
- justify-content: flex-start;
- gap: 32px;
- }
-
-
- .components-radio-control {
- &__option {
- gap: 12px;
-
- input {
- margin: 0;
- }
-
- label {
- @include font(13, 20, 400);
- color: $color-black;
- }
- }
-
- input {
- margin: 0;
- }
+ // Disable interactions with the preview.
+ pointer-events: none;
+ user-select: none;
}
}
}
diff --git a/modules/ppcp-settings/resources/js/Components/App.js b/modules/ppcp-settings/resources/js/Components/App.js
index 69aaabe19..6548f4b1e 100644
--- a/modules/ppcp-settings/resources/js/Components/App.js
+++ b/modules/ppcp-settings/resources/js/Components/App.js
@@ -1,15 +1,20 @@
import { useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
-
import { OnboardingHooks, CommonHooks } from '../data';
import SpinnerOverlay from './ReusableComponents/SpinnerOverlay';
import SendOnlyMessage from './Screens/SendOnlyMessage';
import OnboardingScreen from './Screens/Onboarding';
import SettingsScreen from './Screens/Settings';
+import { initStore as initSettingsStore } from '../data/settings-tab';
+import { useSettingsState } from '../data/settings-tab/hooks';
+
+// Initialize the settings store
+initSettingsStore();
const SettingsApp = () => {
const onboardingProgress = OnboardingHooks.useSteps();
+ const { isReady: settingsIsReady } = useSettingsState();
const {
isReady: merchantIsReady,
merchant: { isSendOnlyCountry },
@@ -21,41 +26,41 @@ const SettingsApp = () => {
event.stopImmediatePropagation();
return undefined;
};
-
window.addEventListener( 'beforeunload', suppressBeforeUnload );
-
return () => {
window.removeEventListener( 'beforeunload', suppressBeforeUnload );
};
}, [] );
const wrapperClass = classNames( 'ppcp-r-app', {
- loading: ! onboardingProgress.isReady,
+ loading: ! onboardingProgress.isReady || ! settingsIsReady,
} );
const Content = useMemo( () => {
- if ( ! onboardingProgress.isReady || ! merchantIsReady ) {
+ if (
+ ! onboardingProgress.isReady ||
+ ! merchantIsReady ||
+ ! settingsIsReady
+ ) {
return (
);
}
-
if ( isSendOnlyCountry ) {
return ;
}
-
if ( ! onboardingProgress.completed ) {
return ;
}
-
return ;
}, [
isSendOnlyCountry,
merchantIsReady,
onboardingProgress.completed,
onboardingProgress.isReady,
+ settingsIsReady,
] );
return
-
-
+
+
+
-
- { props.description && (
+
+ { description && (
) }
- { props?.toggleAdditionalContent &&
- props.children &&
- props.value === props.currentValue && (
-
- { props.children }
-
- ) }
+ { toggleAdditionalContent && children && value === currentValue && (
+
+ { children }
+
+ ) }
);
};
-
-export const handleCheckboxState = ( checked, props ) => {
- let newValue = null;
- if ( ! Array.isArray( props.currentValue ) ) {
- newValue = checked;
- } else if ( checked ) {
- newValue = [ ...props.currentValue, props.value ];
- } else {
- newValue = props.currentValue.filter(
- ( value ) => value !== props.value
- );
- }
- props.changeCallback( newValue );
-};
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/HStack.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/HStack.js
new file mode 100644
index 000000000..2c54ac7da
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/HStack.js
@@ -0,0 +1,26 @@
+/**
+ * Temporary component, until the experimental HStack block editor component is stable.
+ *
+ * @see https://wordpress.github.io/gutenberg/?path=/docs/components-experimental-hstack--docs
+ * @file
+ */
+import classNames from 'classnames';
+
+const HStack = ( { className, spacing = 3, children } ) => {
+ const wrapperClass = classNames(
+ 'components-flex components-h-stack',
+ className
+ );
+
+ const styles = {
+ gap: `calc(${ 4 * spacing }px)`,
+ };
+
+ return (
+
+ { children }
+
+ );
+};
+
+export default HStack;
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js
index ec3ba31ae..46048e071 100644
--- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js
+++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/SettingsBlockElements.js
@@ -1,9 +1,24 @@
+import classNames from 'classnames';
+
// Block Elements
-export const Title = ( { children, className = '' } ) => (
-
- { children }
-
-);
+export const Title = ( {
+ children,
+ altStyle = false,
+ big = false,
+ className = '',
+} ) => {
+ const elementClasses = classNames(
+ 'ppcp-r-settings-block__title',
+ className,
+ {
+ 'style-alt': altStyle,
+ 'style-big': big,
+ }
+ );
+
+ return
{ children };
+};
+
export const TitleWrapper = ( { children } ) => (
{ children }
);
@@ -14,13 +29,28 @@ export const SupplementaryLabel = ( { children } ) => (
);
-export const Description = ( { children, className = '' } ) => (
-
- { children }
-
-);
+export const Description = ( { children, asHtml = false, className = '' } ) => {
+ // Don't output anything if description is empty.
+ if ( ! children ) {
+ return null;
+ }
+
+ const elementClasses = classNames(
+ 'ppcp-r-settings-block__description',
+ className
+ );
+
+ if ( ! asHtml ) {
+ return
{ children };
+ }
+
+ return (
+
+ );
+};
export const Action = ( { children } ) => (
{ children }
@@ -33,11 +63,18 @@ export const Header = ( { children, className = '' } ) => (
);
// Card Elements
-export const Content = ( { children, id = '' } ) => (
-
- { children }
-
-);
+export const Content = ( { children, className = '', id = '' } ) => {
+ const elementClasses = classNames(
+ 'ppcp-r-settings-card__content',
+ className
+ );
+
+ return (
+
+ { children }
+
+ );
+};
export const ContentWrapper = ( { children } ) => (
{ children }
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 1b471fe1e..80d025af8 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js
@@ -1,31 +1,16 @@
-import { useState } from '@wordpress/element';
import ConnectionStatus from './TabSettingsElements/ConnectionStatus';
import CommonSettings from './TabSettingsElements/CommonSettings';
import ExpertSettings from './TabSettingsElements/ExpertSettings';
+import { useSettings } from '../../../data/settings-tab/hooks';
const TabSettings = () => {
- const [ settings, setSettings ] = useState( {
- invoicePrefix: '',
- authorizeOnly: false,
- captureVirtualOnlyOrders: false,
- savePaypalAndVenmo: false,
- saveCreditCardAndDebitCard: false,
- payNowExperience: false,
- sandboxAccountCredentials: false,
- sandboxMode: null,
- sandboxEnabled: false,
- sandboxClientId: '',
- sandboxSecretKey: '',
- sandboxConnected: false,
- logging: false,
- subtotalMismatchFallback: null,
- brandName: '',
- softDescriptor: '',
- paypalLandingPage: null,
- buttonLanguage: '',
- } );
+ const { settings, setSettings } = useSettings();
+
const updateFormValue = ( key, value ) => {
- setSettings( { ...settings, [ key ]: value } );
+ setSettings( {
+ ...settings,
+ [ key ]: value,
+ } );
};
return (
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js
deleted file mode 100644
index 7ddd8be7a..000000000
--- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js
+++ /dev/null
@@ -1,336 +0,0 @@
-import { __, sprintf } from '@wordpress/i18n';
-import { SelectControl, RadioControl } from '@wordpress/components';
-import { PayPalCheckboxGroup } from '../../ReusableComponents/Fields';
-import { useState, useMemo, useEffect } from '@wordpress/element';
-import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js';
-
-import {
- defaultLocationSettings,
- paymentMethodOptions,
- colorOptions,
- shapeOptions,
- buttonLayoutOptions,
- buttonLabelOptions,
-} from '../../../data/settings/tab-styling-data';
-
-const TabStyling = () => {
- const [ location, setLocation ] = useState( 'cart' );
- const [ canRender, setCanRender ] = useState( false );
- const [ locationSettings, setLocationSettings ] = useState( {
- ...defaultLocationSettings,
- } );
-
- // Sometimes buttons won't render. This fixes the timing problem.
- useEffect( () => {
- const handleDOMContentLoaded = () => setCanRender( true );
- if (
- document.readyState === 'interactive' ||
- document.readyState === 'complete'
- ) {
- handleDOMContentLoaded();
- } else {
- document.addEventListener(
- 'DOMContentLoaded',
- handleDOMContentLoaded
- );
- }
- }, [] );
-
- const currentLocationSettings = useMemo( () => {
- return locationSettings[ location ];
- }, [ location, locationSettings ] );
-
- const locationOptions = useMemo( () => {
- return Object.keys( locationSettings ).reduce(
- ( locationOptionsData, key ) => {
- locationOptionsData.push( {
- value: locationSettings[ key ].value,
- label: locationSettings[ key ].label,
- } );
-
- return locationOptionsData;
- },
- []
- );
- }, [] );
-
- const updateButtonSettings = ( key, value ) => {
- setLocationSettings( {
- ...locationSettings,
- [ location ]: {
- ...currentLocationSettings,
- settings: {
- ...currentLocationSettings.settings,
- [ key ]: value,
- },
- },
- } );
- };
-
- const updateButtonStyle = ( key, value ) => {
- setLocationSettings( {
- ...locationSettings,
- [ location ]: {
- ...currentLocationSettings,
- settings: {
- ...currentLocationSettings.settings,
- style: {
- ...currentLocationSettings.settings.style,
- [ key ]: value,
- },
- },
- },
- } );
- };
-
- if ( ! canRender ) {
- return <>>;
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-const TabStylingSection = ( props ) => {
- let sectionTitleClassName = 'ppcp-r-styling__section';
-
- if ( props?.className ) {
- sectionTitleClassName += ` ${ props.className }`;
- }
-
- return (
-
-
{ props.title }
- { props?.description && (
-
- ) }
- { props.children }
-
- );
-};
-
-const SectionIntro = ( { location } ) => {
- const { description, descriptionLink } =
- defaultLocationSettings[ location ];
- const buttonStyleDescription = sprintf( description, descriptionLink );
-
- return (
-
- );
-};
-
-const SectionLocations = ( { locationOptions, location, setLocation } ) => {
- return (
-
- setLocation( newLocation ) }
- label={ __( 'Locations', 'woocommerce-paypal-payments' ) }
- options={ locationOptions }
- />
-
- );
-};
-
-const SectionPaymentMethods = ( {
- locationSettings,
- updateButtonSettings,
-} ) => {
- return (
-
-
-
- updateButtonSettings( 'paymentMethods', newValue )
- }
- currentValue={ locationSettings.settings.paymentMethods }
- />
-
-
- );
-};
-
-const SectionButtonLayout = ( { locationSettings, updateButtonStyle } ) => {
- const buttonLayoutIsAllowed =
- locationSettings.settings.style?.layout &&
- locationSettings.settings.style?.tagline === false;
- return (
- buttonLayoutIsAllowed && (
-
-
- updateButtonStyle( 'layout', newValue )
- }
- selected={ locationSettings.settings.style.layout }
- options={ buttonLayoutOptions }
- />
-
- )
- );
-};
-
-const SectionButtonShape = ( { locationSettings, updateButtonStyle } ) => {
- return (
-
-
- updateButtonStyle( 'shape', newValue )
- }
- selected={ locationSettings.settings.style.shape }
- options={ shapeOptions }
- />
-
- );
-};
-
-const SectionButtonLabel = ( { locationSettings, updateButtonStyle } ) => {
- return (
-
-
- updateButtonStyle( 'label', newValue )
- }
- value={ locationSettings.settings.style.label }
- label={ __( 'Button Label', 'woocommerce-paypal-payments' ) }
- options={ buttonLabelOptions }
- />
-
- );
-};
-
-const SectionButtonColor = ( { locationSettings, updateButtonStyle } ) => {
- return (
-
-
- updateButtonStyle( 'color', newValue )
- }
- value={ locationSettings.settings.style.color }
- options={ colorOptions }
- />
-
- );
-};
-
-const SectionButtonTagline = ( { locationSettings, updateButtonStyle } ) => {
- const taglineIsAllowed =
- locationSettings.settings.style.hasOwnProperty( 'tagline' ) &&
- locationSettings.settings.style?.layout === 'horizontal';
-
- return (
- taglineIsAllowed && (
-
- {
- updateButtonStyle( 'tagline', newValue );
- } }
- currentValue={ locationSettings.settings.style.tagline }
- />
-
- )
- );
-};
-
-const SectionButtonPreview = ( { locationSettings } ) => {
- return (
-
-
- Error
-
-
- );
-};
-
-export default TabStyling;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js
index f195ea1b1..9023a1f9b 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js
@@ -1,16 +1,34 @@
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
+import { CommonHooks, StylingHooks } from '../../../../data';
import TopNavigation from '../../../ReusableComponents/TopNavigation';
+import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
const SettingsNavigation = () => {
+ const { withActivity } = CommonHooks.useBusyState();
+
+ // Todo: Implement other stores here.
+ const { persist: persistStyling } = StylingHooks.useStore();
+
+ const handleSaveClick = () => {
+ // Todo: Add other stores here.
+ withActivity(
+ 'persist-styling',
+ 'Save styling details',
+ persistStyling
+ );
+ };
+
const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' );
return (
-
+
+
+
);
};
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonColor.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonColor.js
new file mode 100644
index 000000000..29b98069c
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonColor.js
@@ -0,0 +1,20 @@
+import { __ } from '@wordpress/i18n';
+
+import { StylingHooks } from '../../../../../../data';
+import { SelectStylingSection } from '../Layout';
+
+const SectionButtonColor = ( { location } ) => {
+ const { color, setColor, choices } = StylingHooks.useColorProps( location );
+
+ return (
+
+ );
+};
+
+export default SectionButtonColor;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLabel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLabel.js
new file mode 100644
index 000000000..de7c1a103
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLabel.js
@@ -0,0 +1,20 @@
+import { __ } from '@wordpress/i18n';
+
+import { StylingHooks } from '../../../../../../data';
+import { SelectStylingSection } from '../Layout';
+
+const SectionButtonLabel = ( { location } ) => {
+ const { label, setLabel, choices } = StylingHooks.useLabelProps( location );
+
+ return (
+
+ );
+};
+
+export default SectionButtonLabel;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js
new file mode 100644
index 000000000..1a284f72a
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonLayout.js
@@ -0,0 +1,29 @@
+import { __ } from '@wordpress/i18n';
+
+import { StylingHooks } from '../../../../../../data';
+import { RadiobuttonStylingSection } from '../Layout';
+import { Tagline } from './index';
+
+const SectionButtonLayout = ( { location } ) => {
+ const { isAvailable, layout, setLayout, choices } =
+ StylingHooks.useLayoutProps( location );
+
+ if ( ! isAvailable ) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default SectionButtonLayout;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonShape.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonShape.js
new file mode 100644
index 000000000..71bc5bebe
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/ButtonShape.js
@@ -0,0 +1,20 @@
+import { __ } from '@wordpress/i18n';
+
+import { StylingHooks } from '../../../../../../data';
+import { RadiobuttonStylingSection } from '../Layout';
+
+const SectionButtonShape = ( { location } ) => {
+ const { shape, setShape, choices } = StylingHooks.useShapeProps( location );
+
+ return (
+
+ );
+};
+
+export default SectionButtonShape;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js
new file mode 100644
index 000000000..a747e93b3
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/LocationSelector.js
@@ -0,0 +1,62 @@
+import { __ } from '@wordpress/i18n';
+import { Button } from '@wordpress/components';
+import { help } from '@wordpress/icons';
+
+import { StylingHooks } from '../../../../../../data';
+import {
+ SelectStylingSection,
+ StylingSection,
+ CheckboxStylingSection,
+} from '../Layout';
+
+const LocationSelector = ( { location, setLocation } ) => {
+ const { choices, details, isActive, setActive } =
+ StylingHooks.useLocationProps( location );
+
+ const activateCheckbox = {
+ value: 'active',
+ label: __(
+ 'Enable payment methods in this location',
+ 'woocommerce-paypal-payments'
+ ),
+ };
+
+ return (
+ <>
+
+
+ { details.link && (
+
+ ) }
+
+
+ >
+ );
+};
+
+export default LocationSelector;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js
new file mode 100644
index 000000000..0397d34c3
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/PaymentMethods.js
@@ -0,0 +1,21 @@
+import { __ } from '@wordpress/i18n';
+
+import { StylingHooks } from '../../../../../../data';
+import { CheckboxStylingSection } from '../Layout';
+
+const SectionPaymentMethods = ( { location } ) => {
+ const { paymentMethods, setPaymentMethods, choices } =
+ StylingHooks.usePaymentMethodProps( location );
+
+ return (
+
+ );
+};
+
+export default SectionPaymentMethods;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/Tagline.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/Tagline.js
new file mode 100644
index 000000000..88912455b
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/Tagline.js
@@ -0,0 +1,33 @@
+import { __ } from '@wordpress/i18n';
+
+import { StylingHooks } from '../../../../../../data';
+import { CheckboxStylingSection } from '../Layout';
+
+const SectionTagline = ( { location } ) => {
+ const { isAvailable, tagline, setTagline } =
+ StylingHooks.useTaglineProps( location );
+
+ if ( ! isAvailable ) {
+ return null;
+ }
+
+ const checkbox = {
+ value: 'active',
+ label: __(
+ 'Show tagline below buttons',
+ 'woocommerce-paypal-payments'
+ ),
+ };
+
+ return (
+
+ );
+};
+
+export default SectionTagline;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/index.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/index.js
new file mode 100644
index 000000000..4529fb9eb
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Content/index.js
@@ -0,0 +1,7 @@
+export { default as LocationSelector } from './LocationSelector';
+export { default as ButtonColor } from './ButtonColor';
+export { default as ButtonLabel } from './ButtonLabel';
+export { default as ButtonLayout } from './ButtonLayout';
+export { default as ButtonShape } from './ButtonShape';
+export { default as PaymentMethods } from './PaymentMethods';
+export { default as Tagline } from './Tagline';
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSection.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSection.js
new file mode 100644
index 000000000..b9fefc52e
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSection.js
@@ -0,0 +1,34 @@
+import SettingsBlock from '../../../../../ReusableComponents/SettingsBlocks/SettingsBlock';
+import {
+ Description,
+ Header,
+ Title,
+ Content,
+} from '../../../../../ReusableComponents/SettingsBlocks';
+
+const StylingSection = ( {
+ title,
+ bigTitle = false,
+ className = '',
+ description = '',
+ separatorAndGap = true,
+ children,
+} ) => {
+ return (
+
+
+
+ { title }
+
+ { description }
+
+
+ { children }
+
+ );
+};
+
+export default StylingSection;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithCheckboxes.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithCheckboxes.js
new file mode 100644
index 000000000..d3ca92db4
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithCheckboxes.js
@@ -0,0 +1,39 @@
+import classNames from 'classnames';
+
+import { CheckboxGroup } from '../../../../../ReusableComponents/Fields';
+import HStack from '../../../../../ReusableComponents/HStack';
+import StylingSection from './StylingSection';
+
+const StylingSectionWithCheckboxes = ( {
+ title,
+ className = '',
+ description = '',
+ separatorAndGap = true,
+ options,
+ value,
+ onChange,
+ children,
+} ) => {
+ className = classNames( 'has-checkboxes', className );
+
+ return (
+
+
+
+
+
+ { children }
+
+ );
+};
+
+export default StylingSectionWithCheckboxes;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithRadiobuttons.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithRadiobuttons.js
new file mode 100644
index 000000000..337e2b30f
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithRadiobuttons.js
@@ -0,0 +1,39 @@
+import { RadioControl } from '@wordpress/components';
+import classNames from 'classnames';
+
+import HStack from '../../../../../ReusableComponents/HStack';
+import StylingSection from './StylingSection';
+
+const StylingSectionWithRadiobuttons = ( {
+ title,
+ className = '',
+ description = '',
+ separatorAndGap = true,
+ options,
+ selected,
+ onChange,
+ children,
+} ) => {
+ className = classNames( 'has-radio-buttons', className );
+
+ return (
+
+
+
+
+
+ { children }
+
+ );
+};
+
+export default StylingSectionWithRadiobuttons;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithSelect.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithSelect.js
new file mode 100644
index 000000000..2b0ef8031
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/StylingSectionWithSelect.js
@@ -0,0 +1,37 @@
+import { SelectControl } from '@wordpress/components';
+import classNames from 'classnames';
+
+import StylingSection from './StylingSection';
+
+const StylingSectionWithSelect = ( {
+ title,
+ className = '',
+ description = '',
+ separatorAndGap = true,
+ options,
+ value,
+ onChange,
+ children,
+} ) => {
+ className = classNames( 'has-select', className );
+
+ return (
+
+
+
+ { children }
+
+ );
+};
+
+export default StylingSectionWithSelect;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/index.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/index.js
new file mode 100644
index 000000000..ea856552d
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/Layout/index.js
@@ -0,0 +1,4 @@
+export { default as StylingSection } from './StylingSection';
+export { default as CheckboxStylingSection } from './StylingSectionWithCheckboxes';
+export { default as RadiobuttonStylingSection } from './StylingSectionWithRadiobuttons';
+export { default as SelectStylingSection } from './StylingSectionWithSelect';
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/PreviewPanel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/PreviewPanel.js
new file mode 100644
index 000000000..8fde7aa98
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/PreviewPanel.js
@@ -0,0 +1,64 @@
+import { PayPalButtons, PayPalScriptProvider } from '@paypal/react-paypal-js';
+import { STYLING_PAYMENT_METHODS, StylingHooks } from '../../../../../data';
+import { useMemo } from '@wordpress/element';
+
+const PREVIEW_CLIENT_ID = 'test';
+const PREVIEW_MERCHANT_ID = 'QTQX5NP6N9WZU';
+
+const PreviewPanel = ( { location } ) => {
+ const { paymentMethods } = StylingHooks.usePaymentMethodProps( location );
+ const { layout } = StylingHooks.useLayoutProps( location );
+ const { shape } = StylingHooks.useShapeProps( location );
+ const { label } = StylingHooks.useLabelProps( location );
+ const { color } = StylingHooks.useColorProps( location );
+ const { tagline } = StylingHooks.useTaglineProps( location );
+
+ const style = useMemo(
+ () => ( {
+ layout,
+ shape,
+ label,
+ color,
+ tagline,
+ } ),
+ [ color, label, layout, shape, tagline ]
+ );
+
+ const disableFunding = useMemo( () => {
+ const disabled = [ 'card' ];
+ Object.values( STYLING_PAYMENT_METHODS )
+ .filter( ( method ) => method.isFunding )
+ .filter( ( method ) => ! paymentMethods.includes( method.value ) )
+ .forEach( ( method ) => {
+ disabled.push( method.value );
+ } );
+ return disabled;
+ }, [ paymentMethods ] );
+
+ // TODO: Changes in the providerOptions are not reflected on the page.
+ const providerOptions = useMemo(
+ () => ( {
+ clientId: PREVIEW_CLIENT_ID,
+ merchantId: PREVIEW_MERCHANT_ID,
+ components: 'buttons',
+ 'disable-funding': disableFunding.join( ',' ),
+ 'buyer-country': 'US', // Todo: simulate shop country here?
+ currency: 'USD',
+ } ),
+ [ disableFunding ]
+ );
+
+ return (
+
+ );
+};
+
+export default PreviewPanel;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js
new file mode 100644
index 000000000..dd5b383b8
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Styling/SettingsPanel.js
@@ -0,0 +1,41 @@
+import {
+ LocationSelector,
+ PaymentMethods,
+ ButtonLayout,
+ ButtonShape,
+ ButtonLabel,
+ ButtonColor,
+} from './Content';
+import { StylingHooks } from '../../../../../data';
+
+const SettingsPanel = ( { location, setLocation } ) => {
+ const { isActive } = StylingHooks.useLocationProps( location );
+
+ const LocationDetails = () => {
+ if ( ! isActive ) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+export default SettingsPanel;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js
new file mode 100644
index 000000000..48a24a2ee
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabStyling.js
@@ -0,0 +1,16 @@
+import { StylingHooks } from '../../../../data';
+import PreviewPanel from '../Components/Styling/PreviewPanel';
+import SettingsPanel from '../Components/Styling/SettingsPanel';
+
+const TabStyling = () => {
+ const { location, setLocation } = StylingHooks.useStylingLocation();
+
+ return (
+
+ );
+};
+
+export default TabStyling;
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/index.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/index.js
index fa19dc6e4..85d4cf5bd 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/index.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/index.js
@@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n';
import TabOverview from '../../Overview/TabOverview';
import TabPaymentMethods from '../../Overview/TabPaymentMethods';
import TabSettings from '../../Overview/TabSettings';
-import TabStyling from '../../Overview/TabStyling';
+import TabStyling from './TabStyling';
import TabPayLaterMessaging from '../../Overview/TabPayLaterMessaging';
/**
diff --git a/modules/ppcp-settings/resources/js/data/_example/actions.js b/modules/ppcp-settings/resources/js/data/_example/actions.js
index 7360424f4..59d68d37c 100644
--- a/modules/ppcp-settings/resources/js/data/_example/actions.js
+++ b/modules/ppcp-settings/resources/js/data/_example/actions.js
@@ -36,28 +36,37 @@ export const hydrate = ( payload ) => ( {
payload,
} );
+/**
+ * Generic transient-data updater.
+ *
+ * @param {string} prop Name of the property to update.
+ * @param {any} value The new value of the property.
+ * @return {Action} The action.
+ */
+export const setTransient = ( prop, value ) => ( {
+ type: ACTION_TYPES.SET_TRANSIENT,
+ payload: { [ prop ]: value },
+} );
+
+/**
+ * Generic persistent-data updater.
+ *
+ * @param {string} prop Name of the property to update.
+ * @param {any} value The new value of the property.
+ * @return {Action} The action.
+ */
+export const setPersistent = ( prop, value ) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: { [ prop ]: value },
+} );
+
/**
* Transient. Marks the store as "ready", i.e., fully initialized.
*
* @param {boolean} isReady
* @return {Action} The action.
*/
-export const setIsReady = ( isReady ) => ( {
- type: ACTION_TYPES.SET_TRANSIENT,
- payload: { isReady },
-} );
-
-/**
- * Persistent. Sets a sample value.
- * TODO: Replace with a real action/property.
- *
- * @param {string} value
- * @return {Action} The action.
- */
-export const setSampleValue = ( value ) => ( {
- type: ACTION_TYPES.SET_PERSISTENT,
- payload: { sampleValue: value },
-} );
+export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
/**
* Side effect. Triggers the persistence of store data to the server.
diff --git a/modules/ppcp-settings/resources/js/data/_example/hooks.js b/modules/ppcp-settings/resources/js/data/_example/hooks.js
index 394fceb7e..b6878c2a9 100644
--- a/modules/ppcp-settings/resources/js/data/_example/hooks.js
+++ b/modules/ppcp-settings/resources/js/data/_example/hooks.js
@@ -7,39 +7,24 @@
* @file
*/
-import { useSelect, useDispatch } from '@wordpress/data';
+import { useDispatch } from '@wordpress/data';
+import { createHooksForStore } from '../utils';
import { STORE_NAME } from './constants';
-const useTransient = ( key ) =>
- useSelect(
- ( select ) => select( STORE_NAME ).transientData()?.[ key ],
- [ key ]
- );
-
-const usePersistent = ( key ) =>
- useSelect(
- ( select ) => select( STORE_NAME ).persistentData()?.[ key ],
- [ key ]
- );
-
const useHooks = () => {
- const {
- persist,
-
- // TODO: Replace with real property.
- setSampleValue,
- } = useDispatch( STORE_NAME );
+ const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
+ const { persist } = useDispatch( STORE_NAME );
// Read-only flags and derived state.
// Nothing here yet.
// Transient accessors.
- const isReady = useTransient( 'isReady' );
+ const [ isReady ] = useTransient( 'isReady' );
// Persistent accessors.
// TODO: Replace with real property.
- const sampleValue = usePersistent( 'sampleValue' );
+ const [ sampleValue, setSampleValue ] = usePersistent( 'sampleValue' );
return {
persist,
diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js
index 8f2b14812..cf7837d54 100644
--- a/modules/ppcp-settings/resources/js/data/common/actions.js
+++ b/modules/ppcp-settings/resources/js/data/common/actions.js
@@ -36,16 +36,37 @@ export const hydrate = ( payload ) => ( {
payload,
} );
+/**
+ * Generic transient-data updater.
+ *
+ * @param {string} prop Name of the property to update.
+ * @param {any} value The new value of the property.
+ * @return {Action} The action.
+ */
+export const setTransient = ( prop, value ) => ( {
+ type: ACTION_TYPES.SET_TRANSIENT,
+ payload: { [ prop ]: value },
+} );
+
+/**
+ * Generic persistent-data updater.
+ *
+ * @param {string} prop Name of the property to update.
+ * @param {any} value The new value of the property.
+ * @return {Action} The action.
+ */
+export const setPersistent = ( prop, value ) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: { [ prop ]: value },
+} );
+
/**
* Transient. Marks the onboarding details as "ready", i.e., fully initialized.
*
* @param {boolean} isReady
* @return {Action} The action.
*/
-export const setIsReady = ( isReady ) => ( {
- type: ACTION_TYPES.SET_TRANSIENT,
- payload: { isReady },
-} );
+export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
/**
* Transient. Sets the active settings tab.
@@ -53,21 +74,8 @@ export const setIsReady = ( isReady ) => ( {
* @param {string} activeModal
* @return {Action} The action.
*/
-export const setActiveModal = ( activeModal ) => ( {
- type: ACTION_TYPES.SET_TRANSIENT,
- payload: { activeModal },
-} );
-
-/**
- * Transient. Changes the "saving" flag.
- *
- * @param {boolean} isSaving
- * @return {Action} The action.
- */
-export const setIsSaving = ( isSaving ) => ( {
- type: ACTION_TYPES.SET_TRANSIENT,
- payload: { isSaving },
-} );
+export const setActiveModal = ( activeModal ) =>
+ setTransient( 'activeModal', activeModal );
/**
* Transient (Activity): Marks the start of an async activity
@@ -107,10 +115,8 @@ export const stopActivity = ( id ) => ( {
* @param {boolean} useSandbox
* @return {Action} The action.
*/
-export const setSandboxMode = ( useSandbox ) => ( {
- type: ACTION_TYPES.SET_PERSISTENT,
- payload: { useSandbox },
-} );
+export const setSandboxMode = ( useSandbox ) =>
+ setPersistent( 'useSandbox', useSandbox );
/**
* Persistent. Toggles the "Manual Connection" mode on or off.
@@ -118,10 +124,8 @@ export const setSandboxMode = ( useSandbox ) => ( {
* @param {boolean} useManualConnection
* @return {Action} The action.
*/
-export const setManualConnectionMode = ( useManualConnection ) => ( {
- type: ACTION_TYPES.SET_PERSISTENT,
- payload: { useManualConnection },
-} );
+export const setManualConnectionMode = ( useManualConnection ) =>
+ setPersistent( 'useManualConnection', useManualConnection );
/**
* Side effect. Saves the persistent details to the WP database.
diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js
index 8fcf3f6a9..796d1c50c 100644
--- a/modules/ppcp-settings/resources/js/data/common/hooks.js
+++ b/modules/ppcp-settings/resources/js/data/common/hooks.js
@@ -9,41 +9,31 @@
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback } from '@wordpress/element';
+
+import { createHooksForStore } from '../utils';
import { STORE_NAME } from './constants';
-const useTransient = ( key ) =>
- useSelect(
- ( select ) => select( STORE_NAME ).transientData()?.[ key ],
- [ key ]
- );
-
-const usePersistent = ( key ) =>
- useSelect(
- ( select ) => select( STORE_NAME ).persistentData()?.[ key ],
- [ key ]
- );
-
const useHooks = () => {
+ const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
const {
persist,
- setSandboxMode,
- setManualConnectionMode,
sandboxOnboardingUrl,
productionOnboardingUrl,
authenticateWithCredentials,
authenticateWithOAuth,
- setActiveModal,
startWebhookSimulation,
checkWebhookSimulationState,
} = useDispatch( STORE_NAME );
// Transient accessors.
- const isReady = useTransient( 'isReady' );
- const activeModal = useTransient( 'activeModal' );
+ const [ isReady ] = useTransient( 'isReady' );
+ const [ activeModal, setActiveModal ] = useTransient( 'activeModal' );
// Persistent accessors.
- const isSandboxMode = usePersistent( 'useSandbox' );
- const isManualConnectionMode = usePersistent( 'useManualConnection' );
+ const [ isSandboxMode, setSandboxMode ] = usePersistent( 'useSandbox' );
+ const [ isManualConnectionMode, setManualConnectionMode ] = usePersistent(
+ 'useManualConnection'
+ );
const merchant = useSelect(
( select ) => select( STORE_NAME ).merchant(),
[]
diff --git a/modules/ppcp-settings/resources/js/data/configuration.js b/modules/ppcp-settings/resources/js/data/configuration.js
new file mode 100644
index 000000000..0f3608552
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/configuration.js
@@ -0,0 +1,10 @@
+export { BUSINESS_TYPES, PRODUCT_TYPES } from './onboarding/configuration';
+
+export {
+ STYLING_LOCATIONS,
+ STYLING_PAYMENT_METHODS,
+ STYLING_LABELS,
+ STYLING_COLORS,
+ STYLING_LAYOUTS,
+ STYLING_SHAPES,
+} from './styling/configuration';
diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js
index 587ef036e..393c09db2 100644
--- a/modules/ppcp-settings/resources/js/data/index.js
+++ b/modules/ppcp-settings/resources/js/data/index.js
@@ -1,20 +1,24 @@
import { addDebugTools } from './debug';
import * as Onboarding from './onboarding';
import * as Common from './common';
+import * as Styling from './styling';
import * as Payment from './payment';
Onboarding.initStore();
Common.initStore();
Payment.initStore();
+Styling.initStore();
export const OnboardingHooks = Onboarding.hooks;
export const CommonHooks = Common.hooks;
export const PaymentHooks = Payment.hooks;
+export const StylingHooks = Styling.hooks;
export const OnboardingStoreName = Onboarding.STORE_NAME;
export const CommonStoreName = Common.STORE_NAME;
export const PaymentStoreName = Payment.STORE_NAME;
+export const StylingStoreName = Styling.STORE_NAME;
-export * from './constants';
+export * from './configuration';
-addDebugTools( window.ppcpSettings, [ Onboarding, Common, Payment ] );
+addDebugTools( window.ppcpSettings, [ Onboarding, Common, Payment, Styling ] );
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js
index e9bf8ed5f..8c6f2999f 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js
@@ -36,16 +36,37 @@ export const hydrate = ( payload ) => ( {
payload,
} );
+/**
+ * Generic transient-data updater.
+ *
+ * @param {string} prop Name of the property to update.
+ * @param {any} value The new value of the property.
+ * @return {Action} The action.
+ */
+export const setTransient = ( prop, value ) => ( {
+ type: ACTION_TYPES.SET_TRANSIENT,
+ payload: { [ prop ]: value },
+} );
+
+/**
+ * Generic persistent-data updater.
+ *
+ * @param {string} prop Name of the property to update.
+ * @param {any} value The new value of the property.
+ * @return {Action} The action.
+ */
+export const setPersistent = ( prop, value ) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: { [ prop ]: value },
+} );
+
/**
* Transient. Marks the onboarding details as "ready", i.e., fully initialized.
*
* @param {boolean} isReady
* @return {Action} The action.
*/
-export const setIsReady = ( isReady ) => ( {
- type: ACTION_TYPES.SET_TRANSIENT,
- payload: { isReady },
-} );
+export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
/**
* Transient. Sets the "manualClientId" value.
@@ -53,10 +74,8 @@ export const setIsReady = ( isReady ) => ( {
* @param {string} manualClientId
* @return {Action} The action.
*/
-export const setManualClientId = ( manualClientId ) => ( {
- type: ACTION_TYPES.SET_TRANSIENT,
- payload: { manualClientId },
-} );
+export const setManualClientId = ( manualClientId ) =>
+ setTransient( 'manualClientId', manualClientId );
/**
* Transient. Sets the "manualClientSecret" value.
@@ -64,10 +83,8 @@ export const setManualClientId = ( manualClientId ) => ( {
* @param {string} manualClientSecret
* @return {Action} The action.
*/
-export const setManualClientSecret = ( manualClientSecret ) => ( {
- type: ACTION_TYPES.SET_TRANSIENT,
- payload: { manualClientSecret },
-} );
+export const setManualClientSecret = ( manualClientSecret ) =>
+ setTransient( 'manualClientSecret', manualClientSecret );
/**
* Persistent.Set the "onboarding completed" flag which shows or hides the wizard.
@@ -75,10 +92,8 @@ export const setManualClientSecret = ( manualClientSecret ) => ( {
* @param {boolean} completed
* @return {Action} The action.
*/
-export const setCompleted = ( completed ) => ( {
- type: ACTION_TYPES.SET_PERSISTENT,
- payload: { completed },
-} );
+export const setCompleted = ( completed ) =>
+ setPersistent( 'completed', completed );
/**
* Persistent. Sets the onboarding wizard to a new step.
@@ -86,10 +101,7 @@ export const setCompleted = ( completed ) => ( {
* @param {number} step
* @return {Action} The action.
*/
-export const setStep = ( step ) => ( {
- type: ACTION_TYPES.SET_PERSISTENT,
- payload: { step },
-} );
+export const setStep = ( step ) => setPersistent( 'step', step );
/**
* Persistent. Sets the "isCasualSeller" value.
@@ -97,10 +109,8 @@ export const setStep = ( step ) => ( {
* @param {boolean} isCasualSeller
* @return {Action} The action.
*/
-export const setIsCasualSeller = ( isCasualSeller ) => ( {
- type: ACTION_TYPES.SET_PERSISTENT,
- payload: { isCasualSeller },
-} );
+export const setIsCasualSeller = ( isCasualSeller ) =>
+ setPersistent( 'isCasualSeller', isCasualSeller );
/**
* Persistent. Sets the "areOptionalPaymentMethodsEnabled" value.
@@ -110,10 +120,11 @@ export const setIsCasualSeller = ( isCasualSeller ) => ( {
*/
export const setAreOptionalPaymentMethodsEnabled = (
areOptionalPaymentMethodsEnabled
-) => ( {
- type: ACTION_TYPES.SET_PERSISTENT,
- payload: { areOptionalPaymentMethodsEnabled },
-} );
+) =>
+ setPersistent(
+ 'areOptionalPaymentMethodsEnabled',
+ areOptionalPaymentMethodsEnabled
+ );
/**
* Persistent. Sets the "products" array.
@@ -121,10 +132,8 @@ export const setAreOptionalPaymentMethodsEnabled = (
* @param {string[]} products
* @return {Action} The action.
*/
-export const setProducts = ( products ) => ( {
- type: ACTION_TYPES.SET_PERSISTENT,
- payload: { products },
-} );
+export const setProducts = ( products ) =>
+ setPersistent( 'products', products );
/**
* Side effect. Triggers the persistence of onboarding data to the server.
diff --git a/modules/ppcp-settings/resources/js/data/constants.js b/modules/ppcp-settings/resources/js/data/onboarding/configuration.js
similarity index 51%
rename from modules/ppcp-settings/resources/js/data/constants.js
rename to modules/ppcp-settings/resources/js/data/onboarding/configuration.js
index 5654ad476..4b31689b5 100644
--- a/modules/ppcp-settings/resources/js/data/constants.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/configuration.js
@@ -1,8 +1,24 @@
+/**
+ * Configuration for UI components.
+ *
+ * @file
+ */
+
+/**
+ * Onboarding options for StepBusiness
+ *
+ * @type {Object}
+ */
export const BUSINESS_TYPES = {
CASUAL_SELLER: 'casual_seller',
BUSINESS: 'business',
};
+/**
+ * Onboarding options for StepProducts
+ *
+ * @type {Object}
+ */
export const PRODUCT_TYPES = {
VIRTUAL: 'virtual',
PHYSICAL: 'physical',
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/constants.js b/modules/ppcp-settings/resources/js/data/onboarding/constants.js
index 4b33c6701..396726199 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/constants.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/constants.js
@@ -8,7 +8,7 @@
export const STORE_NAME = 'wc/paypal/onboarding';
/**
- * 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
* See: OnboardingRestEndpoint.php
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js
index c4308c0fa..70999960c 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js
@@ -9,32 +9,14 @@
import { useSelect, useDispatch } from '@wordpress/data';
-import { PRODUCT_TYPES } from '../constants';
+import { createHooksForStore } from '../utils';
+import { PRODUCT_TYPES } from './configuration';
import { STORE_NAME } from './constants';
-const useTransient = ( key ) =>
- useSelect(
- ( select ) => select( STORE_NAME ).transientData()?.[ key ],
- [ key ]
- );
-
-const usePersistent = ( key ) =>
- useSelect(
- ( select ) => select( STORE_NAME ).persistentData()?.[ key ],
- [ key ]
- );
-
const useHooks = () => {
- const {
- persist,
- setStep,
- setCompleted,
- setIsCasualSeller,
- setManualClientId,
- setManualClientSecret,
- setAreOptionalPaymentMethodsEnabled,
- setProducts,
- } = useDispatch( STORE_NAME );
+ const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
+
+ const { persist } = useDispatch( STORE_NAME );
// Read-only flags and derived state.
const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] );
@@ -44,18 +26,22 @@ const useHooks = () => {
);
// Transient accessors.
- const isReady = useTransient( 'isReady' );
- const manualClientId = useTransient( 'manualClientId' );
- const manualClientSecret = useTransient( 'manualClientSecret' );
+ const [ isReady ] = useTransient( 'isReady' );
+ const [ manualClientId, setManualClientId ] =
+ useTransient( 'manualClientId' );
+ const [ manualClientSecret, setManualClientSecret ] =
+ useTransient( 'manualClientSecret' );
// Persistent accessors.
- const step = usePersistent( 'step' );
- const completed = usePersistent( 'completed' );
- const isCasualSeller = usePersistent( 'isCasualSeller' );
- const areOptionalPaymentMethodsEnabled = usePersistent(
- 'areOptionalPaymentMethodsEnabled'
- );
- const products = usePersistent( 'products' );
+ const [ step, setStep ] = usePersistent( 'step' );
+ const [ completed, setCompleted ] = usePersistent( 'completed' );
+ const [ isCasualSeller, setIsCasualSeller ] =
+ usePersistent( 'isCasualSeller' );
+ const [
+ areOptionalPaymentMethodsEnabled,
+ setAreOptionalPaymentMethodsEnabled,
+ ] = usePersistent( 'areOptionalPaymentMethodsEnabled' );
+ const [ products, setProducts ] = usePersistent( 'products' );
const savePersistent = async ( setter, value ) => {
setter( value );
diff --git a/modules/ppcp-settings/resources/js/data/settings-tab/action-types.js b/modules/ppcp-settings/resources/js/data/settings-tab/action-types.js
new file mode 100644
index 000000000..d13c0cbcf
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/settings-tab/action-types.js
@@ -0,0 +1,40 @@
+/**
+ * Settings action types
+ *
+ * Defines the constants used for dispatching actions in the settings store.
+ * Each constant represents a unique action type that can be handled by reducers.
+ *
+ * @file
+ */
+
+export default {
+ /**
+ * Represents setting transient (temporary) state data.
+ * These values are not persisted and will reset on page reload.
+ */
+ SET_TRANSIENT: 'ppcp/settings/SET_TRANSIENT',
+
+ /**
+ * Represents setting persistent state data.
+ * These values are meant to be saved to the server and persist between page loads.
+ */
+ SET_PERSISTENT: 'ppcp/settings/SET_PERSISTENT',
+
+ /**
+ * Resets the store state to its initial values.
+ * Used when needing to clear all settings data.
+ */
+ RESET: 'ppcp/settings/RESET',
+
+ /**
+ * Initializes the store with data, typically used during store initialization
+ * to set up the initial state with data from the server.
+ */
+ HYDRATE: 'ppcp/settings/HYDRATE',
+
+ /**
+ * Triggers the persistence of store data to the server.
+ * Used when changes need to be saved to the backend.
+ */
+ DO_PERSIST_DATA: 'ppcp/settings/DO_PERSIST_DATA',
+};
diff --git a/modules/ppcp-settings/resources/js/data/settings-tab/actions.js b/modules/ppcp-settings/resources/js/data/settings-tab/actions.js
new file mode 100644
index 000000000..a13cedeab
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/settings-tab/actions.js
@@ -0,0 +1,71 @@
+/**
+ * Action Creators: Define functions to create action objects.
+ *
+ * These functions update state or trigger side effects (e.g., async operations).
+ * Actions are categorized as Transient, Persistent, or Side effect.
+ *
+ * @file
+ */
+
+import { select } from '@wordpress/data';
+import ACTION_TYPES from './action-types';
+import { STORE_NAME } from './constants';
+
+/**
+ * @typedef {Object} Action An action object that is handled by a reducer or control.
+ * @property {string} type - The action type.
+ * @property {Object?} payload - Optional payload for the action.
+ */
+
+/**
+ * Special. Resets all values in the store to initial defaults.
+ *
+ * @return {Action} The action.
+ */
+export const reset = () => ( {
+ type: ACTION_TYPES.RESET,
+} );
+
+/**
+ * Persistent. Sets the full store details during app initialization.
+ *
+ * @param {Object} payload Initial store data
+ * @return {Action} The action.
+ */
+export const hydrate = ( payload ) => ( {
+ type: ACTION_TYPES.HYDRATE,
+ payload,
+} );
+
+/**
+ * Transient. Marks the store as "ready", i.e., fully initialized.
+ *
+ * @param {boolean} isReady Whether the store is ready
+ * @return {Action} The action.
+ */
+export const setIsReady = ( isReady ) => ( {
+ type: ACTION_TYPES.SET_TRANSIENT,
+ payload: { isReady },
+} );
+
+/**
+ * Persistent. Updates the settings data in the store.
+ *
+ * @param {Object} settings The settings object to store
+ * @return {Action} The action.
+ */
+export const setSettings = ( settings ) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: settings,
+} );
+
+/**
+ * Side effect. Triggers the persistence of store data to the server.
+ * Yields an action with the current persistent data to be saved.
+ *
+ * @return {Action} The action.
+ */
+export const persist = function* () {
+ const data = yield select( STORE_NAME ).persistentData();
+ yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
+};
diff --git a/modules/ppcp-settings/resources/js/data/settings-tab/constants.js b/modules/ppcp-settings/resources/js/data/settings-tab/constants.js
new file mode 100644
index 000000000..dd7faa120
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/settings-tab/constants.js
@@ -0,0 +1,28 @@
+/**
+ * Name of the Redux store module.
+ *
+ * Used by: Reducer, Selector, Index
+ *
+ * @type {string}
+ */
+export const STORE_NAME = 'wc/paypal/settings';
+
+/**
+ * REST path to hydrate data of this module by loading data from the WP DB.
+ *
+ * Used by: Resolvers
+ * See: SettingsRestEndpoint.php
+ *
+ * @type {string}
+ */
+export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/settings';
+
+/**
+ * REST path to persist data of this module to the WP DB.
+ *
+ * Used by: Controls
+ * See: SettingsRestEndpoint.php
+ *
+ * @type {string}
+ */
+export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/settings';
diff --git a/modules/ppcp-settings/resources/js/data/settings-tab/controls.js b/modules/ppcp-settings/resources/js/data/settings-tab/controls.js
new file mode 100644
index 000000000..11e5e6a04
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/settings-tab/controls.js
@@ -0,0 +1,34 @@
+/**
+ * Controls: Implement side effects, typically asynchronous operations.
+ *
+ * Controls use ACTION_TYPES keys as identifiers.
+ * They are triggered by corresponding actions and handle external interactions.
+ *
+ * @file
+ */
+
+import apiFetch from '@wordpress/api-fetch';
+import { REST_PERSIST_PATH } from './constants';
+import ACTION_TYPES from './action-types';
+
+/**
+ * Control handlers for settings store actions.
+ * Each handler maps to an ACTION_TYPE and performs the corresponding async operation.
+ */
+export const controls = {
+ /**
+ * Persists settings data to the server via REST API.
+ * Triggered by the DO_PERSIST_DATA action to save settings changes.
+ *
+ * @param {Object} action The action object
+ * @param {Object} action.data The settings data to persist
+ * @return {Promise