mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-06 10:55:00 +08:00
🔀 Merge branch 'trunk'
# Conflicts: # modules/ppcp-settings/src/SettingsModule.php
This commit is contained in:
commit
d1ce0abbde
62 changed files with 2466 additions and 1033 deletions
|
@ -43,3 +43,14 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $gap;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -55,6 +55,8 @@ $card-vertical-gap: 48px;
|
||||||
--color-gray-100: #{$color-gray-100};
|
--color-gray-100: #{$color-gray-100};
|
||||||
--color-gradient-dark: #{$color-gradient-dark};
|
--color-gradient-dark: #{$color-gradient-dark};
|
||||||
|
|
||||||
|
--color-preview-background: #FAF8F5;
|
||||||
|
--color-separators: #{$color-gray-200};
|
||||||
--color-text-title: #{$color-gray-900};
|
--color-text-title: #{$color-gray-900};
|
||||||
--color-text-main: #{$color-text-text};
|
--color-text-main: #{$color-text-text};
|
||||||
--color-text-teriary: #{$color-text-tertiary};
|
--color-text-teriary: #{$color-text-tertiary};
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
@import './reusable-components/busy-state';
|
@import './reusable-components/busy-state';
|
||||||
@import './reusable-components/button';
|
@import './reusable-components/button';
|
||||||
@import './reusable-components/fields';
|
@import './reusable-components/fields';
|
||||||
|
@import './reusable-components/hstack';
|
||||||
@import './reusable-components/navigation';
|
@import './reusable-components/navigation';
|
||||||
@import './reusable-components/onboarding-header';
|
@import './reusable-components/onboarding-header';
|
||||||
@import './reusable-components/payment-method-icons';
|
@import './reusable-components/payment-method-icons';
|
||||||
|
|
|
@ -1,27 +1,96 @@
|
||||||
.ppcp-r {
|
.ppcp-r__radio-value {
|
||||||
|
@include hide-input-field;
|
||||||
|
|
||||||
&__radio-value {
|
&:checked + .ppcp-r__radio-presentation {
|
||||||
@include hide-input-field;
|
position: relative;
|
||||||
|
|
||||||
&:checked + .ppcp-r__radio-presentation {
|
&::before {
|
||||||
position: relative;
|
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 {
|
.ppcp-r__radio-presentation {
|
||||||
content: '';
|
@include fake-input-field(20px);
|
||||||
width: 12px;
|
}
|
||||||
height: 12px;
|
|
||||||
border-radius: 12px;
|
.ppcp-r__checkbox-presentation {
|
||||||
background-color: $color-blueberry;
|
@include fake-input-field(2px);
|
||||||
display: block;
|
}
|
||||||
position: absolute;
|
|
||||||
transform: translate(-50%, -50%);
|
.ppcp-r__radio-wrapper {
|
||||||
left: 50%;
|
display: flex;
|
||||||
top: 50%;
|
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;
|
position: relative;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
@ -30,7 +99,7 @@
|
||||||
|
|
||||||
&:checked {
|
&:checked {
|
||||||
background-color: $color-blueberry;
|
background-color: $color-blueberry;
|
||||||
border-color:$color-blueberry;
|
border-color: $color-blueberry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,78 +112,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__radio-presentation {
|
// Custom styles.
|
||||||
@include fake-input-field(20px);
|
.components-form-toggle.is-checked > .components-form-toggle__track {
|
||||||
|
background-color: $color-blueberry;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__checkbox-presentation {
|
.ppcp-r-vertical-text-control {
|
||||||
@include fake-input-field(2px);
|
.components-base-control__field {
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
&__radio-wrapper {
|
gap: 0;
|
||||||
display: flex;
|
margin: 0;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
.ppcp-r-settings-block {
|
.ppcp-r-settings-block {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px 0;
|
gap: var(--block-item-gap, 16px);
|
||||||
|
|
||||||
&.ppcp-r-settings-block__input,
|
&.ppcp-r-settings-block__input,
|
||||||
&.ppcp-r-settings-block__select {
|
&.ppcp-r-settings-block__select {
|
||||||
|
@ -16,8 +16,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|
||||||
&:not(:last-child):not(.ppcp-r-settings-block--accordion__header) {
|
&:not(:last-child) {
|
||||||
padding-bottom: 6px;
|
padding-bottom: var(--block-header-gap, 6px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,15 @@
|
||||||
display: block;
|
display: block;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&.style-alt {
|
||||||
|
@include font(14, 16, 600);
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.style-big {
|
||||||
|
@include font(16, 20, 600);
|
||||||
|
}
|
||||||
|
|
||||||
.ppcp-r-title-badge {
|
.ppcp-r-title-badge {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
|
@ -88,9 +97,18 @@
|
||||||
margin-left: 5px;
|
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) {
|
+ .ppcp-r-settings-block:not(.no-gap) {
|
||||||
margin-top: 32px;
|
margin-top: var(--block-separator-gap, 32px);
|
||||||
padding-top: 32px;
|
padding-top: var(--block-separator-gap, 32px);
|
||||||
border-top: 1px solid var(--color-gray-200);
|
border-top: 1px solid var(--color-gray-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
@import './settings/input';
|
@import './settings/input';
|
||||||
|
@import './settings/connection-status';
|
||||||
@import './settings/tab-styling';
|
@import './settings/tab-styling';
|
||||||
@import './settings/tab-paylater-configurator';
|
@import './settings/tab-paylater-configurator';
|
||||||
|
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.ppcp-r-todo-item__inner {
|
.ppcp-r-todo-item__inner {
|
||||||
.ppcp-r-todo-item__description {
|
.ppcp-r-todo-item__description {
|
||||||
color: $color-text-text;
|
color: $color-text-text;
|
||||||
|
@ -117,91 +119,7 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
margin-top:24px;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
.ppcp-r-paylater-configurator {
|
.ppcp-r-paylater-configurator {
|
||||||
display: flex;
|
display: flex;
|
||||||
border: 1px solid $color-gray-200;
|
border: 1px solid var(--color-separators);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
|
|
@ -1,121 +1,96 @@
|
||||||
.ppcp-r-styling {
|
.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;
|
display: flex;
|
||||||
border: 1px solid $color-gray-200;
|
border: 1px solid var(--color-separators);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&__section:not(:last-child) {
|
.ppcp-r-settings-block {
|
||||||
border-bottom: 1px solid black;
|
&.header-section {
|
||||||
padding-bottom: 24px;
|
margin-bottom: 6px
|
||||||
margin-bottom: 28px;
|
}
|
||||||
border-bottom: 1px solid $color-gray-600;
|
|
||||||
|
&.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 {
|
/* The settings-panel (left side) */
|
||||||
@include font(14, 20, 600);
|
.settings-panel {
|
||||||
color: $color-gray-800;
|
width: var(--panel-width);
|
||||||
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;
|
|
||||||
padding: 48px;
|
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 {
|
/* The preview area (right side) */
|
||||||
width: calc(100% - 422px);
|
.preview-panel {
|
||||||
background-color: #FAF8F5;
|
width: calc(100% - var(--panel-width));
|
||||||
display: flex;
|
background-color: var(--color-preview-background);
|
||||||
align-items: center;
|
z-index: 0;
|
||||||
|
|
||||||
&-inner {
|
.preview-panel-inner {
|
||||||
|
position: sticky;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
height: calc(100vh - var(--preview-height-reduction));
|
||||||
}
|
top: var(--sticky-offset-top);
|
||||||
|
|
||||||
&__section--rc {
|
// Disable interactions with the preview.
|
||||||
.ppcp-r-styling__title {
|
pointer-events: none;
|
||||||
@include font(13, 20, 600);
|
user-select: none;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,107 +1,138 @@
|
||||||
import { CheckboxControl } from '@wordpress/components';
|
import { CheckboxControl } from '@wordpress/components';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export const PayPalCheckbox = ( props ) => {
|
export const PayPalCheckbox = ( {
|
||||||
let isChecked = null;
|
currentValue,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
checked = null,
|
||||||
|
disabled = null,
|
||||||
|
changeCallback,
|
||||||
|
} ) => {
|
||||||
|
let isChecked = checked;
|
||||||
|
|
||||||
if ( Array.isArray( props.currentValue ) ) {
|
if ( null === isChecked ) {
|
||||||
isChecked = props.currentValue.includes( props.value );
|
if ( Array.isArray( currentValue ) ) {
|
||||||
} else {
|
isChecked = currentValue.includes( value );
|
||||||
isChecked = props.currentValue;
|
} else {
|
||||||
|
isChecked = currentValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const className = classNames( { 'is-disabled': disabled } );
|
||||||
|
|
||||||
|
const onChange = ( newState ) => {
|
||||||
|
let newValue;
|
||||||
|
|
||||||
|
if ( ! Array.isArray( currentValue ) ) {
|
||||||
|
newValue = newState;
|
||||||
|
} else if ( newState ) {
|
||||||
|
newValue = [ ...currentValue, value ];
|
||||||
|
} else {
|
||||||
|
newValue = currentValue.filter(
|
||||||
|
( optionValue ) => optionValue !== value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeCallback( newValue );
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ppcp-r__checkbox">
|
<CheckboxControl
|
||||||
<CheckboxControl
|
label={ label }
|
||||||
label={ props?.label ? props.label : '' }
|
value={ value }
|
||||||
value={ props.value }
|
checked={ isChecked }
|
||||||
checked={ isChecked }
|
disabled={ disabled }
|
||||||
onChange={ ( checked ) =>
|
onChange={ onChange }
|
||||||
handleCheckboxState( checked, props )
|
className={ className }
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PayPalCheckboxGroup = ( props ) => {
|
export const CheckboxGroup = ( { options, value, onChange } ) => (
|
||||||
const renderCheckboxGroup = () => {
|
<>
|
||||||
return props.value.map( ( checkbox ) => {
|
{ options.map( ( checkbox ) => (
|
||||||
return (
|
<PayPalCheckbox
|
||||||
<PayPalCheckbox
|
key={ checkbox.value }
|
||||||
label={ checkbox.label }
|
label={ checkbox.label }
|
||||||
value={ checkbox.value }
|
value={ checkbox.value }
|
||||||
key={ checkbox.value }
|
checked={ checkbox.checked }
|
||||||
currentValue={ props.currentValue }
|
disabled={ checkbox.disabled }
|
||||||
changeCallback={ props.changeCallback }
|
description={ checkbox.description }
|
||||||
/>
|
tooltip={ checkbox.tooltip }
|
||||||
);
|
currentValue={ value }
|
||||||
} );
|
changeCallback={ onChange }
|
||||||
};
|
/>
|
||||||
|
) ) }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return <>{ renderCheckboxGroup() }</>;
|
export const PayPalRdb = ( {
|
||||||
};
|
id,
|
||||||
|
name,
|
||||||
export const PayPalRdb = ( props ) => {
|
value,
|
||||||
|
currentValue,
|
||||||
|
handleRdbState,
|
||||||
|
} ) => {
|
||||||
return (
|
return (
|
||||||
<div className="ppcp-r__radio">
|
<div className="ppcp-r__radio">
|
||||||
|
{ /* todo: Can we remove the wrapper div? */ }
|
||||||
<input
|
<input
|
||||||
id={ props?.id }
|
|
||||||
className="ppcp-r__radio-value"
|
className="ppcp-r__radio-value"
|
||||||
type="radio"
|
type="radio"
|
||||||
checked={ props.value === props.currentValue }
|
id={ id }
|
||||||
name={ props.name }
|
checked={ value === currentValue }
|
||||||
value={ props.value }
|
name={ name }
|
||||||
onChange={ () => props.handleRdbState( props.value ) }
|
value={ value }
|
||||||
|
onChange={ () => handleRdbState( value ) }
|
||||||
/>
|
/>
|
||||||
<span className="ppcp-r__radio-presentation"></span>
|
<span className="ppcp-r__radio-presentation"></span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PayPalRdbWithContent = ( props ) => {
|
export const PayPalRdbWithContent = ( {
|
||||||
const className = [ 'ppcp-r__radio-wrapper' ];
|
className,
|
||||||
|
id,
|
||||||
if ( props?.className ) {
|
name,
|
||||||
className.push( props.className );
|
label,
|
||||||
}
|
description,
|
||||||
|
value,
|
||||||
|
currentValue,
|
||||||
|
handleRdbState,
|
||||||
|
toggleAdditionalContent,
|
||||||
|
children,
|
||||||
|
} ) => {
|
||||||
|
const wrapperClasses = classNames( 'ppcp-r__radio-wrapper', className );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ppcp-r__radio-outer-wrapper">
|
<div className="ppcp-r__radio-outer-wrapper">
|
||||||
<div className={ className }>
|
<div className={ wrapperClasses }>
|
||||||
<PayPalRdb { ...props } />
|
<PayPalRdb
|
||||||
|
id={ id }
|
||||||
|
name={ name }
|
||||||
|
value={ value }
|
||||||
|
currentValue={ currentValue }
|
||||||
|
handleRdbState={ handleRdbState }
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="ppcp-r__radio-content">
|
<div className="ppcp-r__radio-content">
|
||||||
<label htmlFor={ props?.id }>{ props.label }</label>
|
<label htmlFor={ id }>{ label }</label>
|
||||||
{ props.description && (
|
{ description && (
|
||||||
<p
|
<p
|
||||||
className="ppcp-r__radio-description"
|
className="ppcp-r__radio-description"
|
||||||
dangerouslySetInnerHTML={ {
|
dangerouslySetInnerHTML={ {
|
||||||
__html: props.description,
|
__html: description,
|
||||||
} }
|
} }
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ props?.toggleAdditionalContent &&
|
{ toggleAdditionalContent && children && value === currentValue && (
|
||||||
props.children &&
|
<div className="ppcp-r__radio-content-additional">
|
||||||
props.value === props.currentValue && (
|
{ children }
|
||||||
<div className="ppcp-r__radio-content-additional">
|
</div>
|
||||||
{ props.children }
|
) }
|
||||||
</div>
|
|
||||||
) }
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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 );
|
|
||||||
};
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
<div className={ wrapperClass } style={ styles }>
|
||||||
|
{ children }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HStack;
|
|
@ -1,9 +1,24 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
// Block Elements
|
// Block Elements
|
||||||
export const Title = ( { children, className = '' } ) => (
|
export const Title = ( {
|
||||||
<span className={ `ppcp-r-settings-block__title ${ className }`.trim() }>
|
children,
|
||||||
{ children }
|
altStyle = false,
|
||||||
</span>
|
big = false,
|
||||||
);
|
className = '',
|
||||||
|
} ) => {
|
||||||
|
const elementClasses = classNames(
|
||||||
|
'ppcp-r-settings-block__title',
|
||||||
|
className,
|
||||||
|
{
|
||||||
|
'style-alt': altStyle,
|
||||||
|
'style-big': big,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return <span className={ elementClasses }>{ children }</span>;
|
||||||
|
};
|
||||||
|
|
||||||
export const TitleWrapper = ( { children } ) => (
|
export const TitleWrapper = ( { children } ) => (
|
||||||
<span className="ppcp-r-settings-block__title-wrapper">{ children }</span>
|
<span className="ppcp-r-settings-block__title-wrapper">{ children }</span>
|
||||||
);
|
);
|
||||||
|
@ -14,13 +29,28 @@ export const SupplementaryLabel = ( { children } ) => (
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Description = ( { children, className = '' } ) => (
|
export const Description = ( { children, asHtml = false, className = '' } ) => {
|
||||||
<span
|
// Don't output anything if description is empty.
|
||||||
className={ `ppcp-r-settings-block__description ${ className }`.trim() }
|
if ( ! children ) {
|
||||||
>
|
return null;
|
||||||
{ children }
|
}
|
||||||
</span>
|
|
||||||
);
|
const elementClasses = classNames(
|
||||||
|
'ppcp-r-settings-block__description',
|
||||||
|
className
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! asHtml ) {
|
||||||
|
return <span className={ elementClasses }>{ children }</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={ elementClasses }
|
||||||
|
dangerouslySetInnerHTML={ { __html: children } }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const Action = ( { children } ) => (
|
export const Action = ( { children } ) => (
|
||||||
<div className="ppcp-r-settings-block__action">{ children }</div>
|
<div className="ppcp-r-settings-block__action">{ children }</div>
|
||||||
|
@ -33,11 +63,18 @@ export const Header = ( { children, className = '' } ) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
// Card Elements
|
// Card Elements
|
||||||
export const Content = ( { children, id = '' } ) => (
|
export const Content = ( { children, className = '', id = '' } ) => {
|
||||||
<div id={ id } className="ppcp-r-settings-card__content">
|
const elementClasses = classNames(
|
||||||
{ children }
|
'ppcp-r-settings-card__content',
|
||||||
</div>
|
className
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id={ id } className={ elementClasses }>
|
||||||
|
{ children }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ContentWrapper = ( { children } ) => (
|
export const ContentWrapper = ( { children } ) => (
|
||||||
<div className="ppcp-r-settings-card__content-wrapper">{ children }</div>
|
<div className="ppcp-r-settings-card__content-wrapper">{ children }</div>
|
||||||
|
|
|
@ -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 (
|
|
||||||
<div className="ppcp-r-styling">
|
|
||||||
<div className="ppcp-r-styling__settings">
|
|
||||||
<SectionIntro location={ location } />
|
|
||||||
<SectionLocations
|
|
||||||
locationOptions={ locationOptions }
|
|
||||||
location={ location }
|
|
||||||
setLocation={ setLocation }
|
|
||||||
/>
|
|
||||||
<SectionPaymentMethods
|
|
||||||
locationSettings={ currentLocationSettings }
|
|
||||||
updateButtonSettings={ updateButtonSettings }
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SectionButtonLayout
|
|
||||||
locationSettings={ currentLocationSettings }
|
|
||||||
updateButtonStyle={ updateButtonStyle }
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SectionButtonShape
|
|
||||||
locationSettings={ currentLocationSettings }
|
|
||||||
updateButtonStyle={ updateButtonStyle }
|
|
||||||
/>
|
|
||||||
<SectionButtonLabel
|
|
||||||
locationSettings={ currentLocationSettings }
|
|
||||||
updateButtonStyle={ updateButtonStyle }
|
|
||||||
/>
|
|
||||||
<SectionButtonColor
|
|
||||||
locationSettings={ currentLocationSettings }
|
|
||||||
updateButtonStyle={ updateButtonStyle }
|
|
||||||
/>
|
|
||||||
<SectionButtonTagline
|
|
||||||
locationSettings={ currentLocationSettings }
|
|
||||||
updateButtonStyle={ updateButtonStyle }
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="ppcp-preview ppcp-r-button-preview ppcp-r-styling__preview">
|
|
||||||
<div className="ppcp-r-styling__preview-inner">
|
|
||||||
<SectionButtonPreview
|
|
||||||
locationSettings={ currentLocationSettings }
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TabStylingSection = ( props ) => {
|
|
||||||
let sectionTitleClassName = 'ppcp-r-styling__section';
|
|
||||||
|
|
||||||
if ( props?.className ) {
|
|
||||||
sectionTitleClassName += ` ${ props.className }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={ sectionTitleClassName }>
|
|
||||||
<span className="ppcp-r-styling__title">{ props.title }</span>
|
|
||||||
{ props?.description && (
|
|
||||||
<p
|
|
||||||
dangerouslySetInnerHTML={ {
|
|
||||||
__html: props.description,
|
|
||||||
} }
|
|
||||||
className="ppcp-r-styling__description"
|
|
||||||
/>
|
|
||||||
) }
|
|
||||||
{ props.children }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SectionIntro = ( { location } ) => {
|
|
||||||
const { description, descriptionLink } =
|
|
||||||
defaultLocationSettings[ location ];
|
|
||||||
const buttonStyleDescription = sprintf( description, descriptionLink );
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TabStylingSection
|
|
||||||
className="ppcp-r-styling__section--rc ppcp-r-styling__section--empty"
|
|
||||||
title={ __( 'Button Styling', 'wooocommerce-paypal-payments' ) }
|
|
||||||
description={ buttonStyleDescription }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SectionLocations = ( { locationOptions, location, setLocation } ) => {
|
|
||||||
return (
|
|
||||||
<TabStylingSection className="ppcp-r-styling__section--rc">
|
|
||||||
<SelectControl
|
|
||||||
className="ppcp-r-styling__select"
|
|
||||||
value={ location }
|
|
||||||
onChange={ ( newLocation ) => setLocation( newLocation ) }
|
|
||||||
label={ __( 'Locations', 'woocommerce-paypal-payments' ) }
|
|
||||||
options={ locationOptions }
|
|
||||||
/>
|
|
||||||
</TabStylingSection>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SectionPaymentMethods = ( {
|
|
||||||
locationSettings,
|
|
||||||
updateButtonSettings,
|
|
||||||
} ) => {
|
|
||||||
return (
|
|
||||||
<TabStylingSection
|
|
||||||
title={ __( 'Payment Methods', 'woocommerce-paypal-payments' ) }
|
|
||||||
className="ppcp-r-styling__section--rc"
|
|
||||||
>
|
|
||||||
<div className="ppcp-r-styling__payment-method-checkboxes">
|
|
||||||
<PayPalCheckboxGroup
|
|
||||||
value={ paymentMethodOptions }
|
|
||||||
changeCallback={ ( newValue ) =>
|
|
||||||
updateButtonSettings( 'paymentMethods', newValue )
|
|
||||||
}
|
|
||||||
currentValue={ locationSettings.settings.paymentMethods }
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TabStylingSection>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SectionButtonLayout = ( { locationSettings, updateButtonStyle } ) => {
|
|
||||||
const buttonLayoutIsAllowed =
|
|
||||||
locationSettings.settings.style?.layout &&
|
|
||||||
locationSettings.settings.style?.tagline === false;
|
|
||||||
return (
|
|
||||||
buttonLayoutIsAllowed && (
|
|
||||||
<TabStylingSection
|
|
||||||
className="ppcp-r-styling__section--rc"
|
|
||||||
title={ __( 'Button Layout', 'woocommerce-paypal-payments' ) }
|
|
||||||
>
|
|
||||||
<RadioControl
|
|
||||||
className="ppcp-r__horizontal-control"
|
|
||||||
onChange={ ( newValue ) =>
|
|
||||||
updateButtonStyle( 'layout', newValue )
|
|
||||||
}
|
|
||||||
selected={ locationSettings.settings.style.layout }
|
|
||||||
options={ buttonLayoutOptions }
|
|
||||||
/>
|
|
||||||
</TabStylingSection>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SectionButtonShape = ( { locationSettings, updateButtonStyle } ) => {
|
|
||||||
return (
|
|
||||||
<TabStylingSection
|
|
||||||
title={ __( 'Shape', 'woocommerce-paypal-payments' ) }
|
|
||||||
className="ppcp-r-styling__section--rc"
|
|
||||||
>
|
|
||||||
<RadioControl
|
|
||||||
className="ppcp-r__horizontal-control"
|
|
||||||
onChange={ ( newValue ) =>
|
|
||||||
updateButtonStyle( 'shape', newValue )
|
|
||||||
}
|
|
||||||
selected={ locationSettings.settings.style.shape }
|
|
||||||
options={ shapeOptions }
|
|
||||||
/>
|
|
||||||
</TabStylingSection>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SectionButtonLabel = ( { locationSettings, updateButtonStyle } ) => {
|
|
||||||
return (
|
|
||||||
<TabStylingSection>
|
|
||||||
<SelectControl
|
|
||||||
className="ppcp-r-styling__select"
|
|
||||||
onChange={ ( newValue ) =>
|
|
||||||
updateButtonStyle( 'label', newValue )
|
|
||||||
}
|
|
||||||
value={ locationSettings.settings.style.label }
|
|
||||||
label={ __( 'Button Label', 'woocommerce-paypal-payments' ) }
|
|
||||||
options={ buttonLabelOptions }
|
|
||||||
/>
|
|
||||||
</TabStylingSection>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SectionButtonColor = ( { locationSettings, updateButtonStyle } ) => {
|
|
||||||
return (
|
|
||||||
<TabStylingSection>
|
|
||||||
<SelectControl
|
|
||||||
className=" ppcp-r-styling__select"
|
|
||||||
label={ __( 'Button Color', 'woocommerce-paypal-payments' ) }
|
|
||||||
onChange={ ( newValue ) =>
|
|
||||||
updateButtonStyle( 'color', newValue )
|
|
||||||
}
|
|
||||||
value={ locationSettings.settings.style.color }
|
|
||||||
options={ colorOptions }
|
|
||||||
/>
|
|
||||||
</TabStylingSection>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SectionButtonTagline = ( { locationSettings, updateButtonStyle } ) => {
|
|
||||||
const taglineIsAllowed =
|
|
||||||
locationSettings.settings.style.hasOwnProperty( 'tagline' ) &&
|
|
||||||
locationSettings.settings.style?.layout === 'horizontal';
|
|
||||||
|
|
||||||
return (
|
|
||||||
taglineIsAllowed && (
|
|
||||||
<TabStylingSection
|
|
||||||
title={ __( 'Tagline', 'woocommerce-paypal-payments' ) }
|
|
||||||
className="ppcp-r-styling__section--rc"
|
|
||||||
>
|
|
||||||
<PayPalCheckboxGroup
|
|
||||||
value={ [
|
|
||||||
{
|
|
||||||
value: 'tagline',
|
|
||||||
label: __(
|
|
||||||
'Enable Tagline',
|
|
||||||
'woocommerce-paypal-payments'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
] }
|
|
||||||
changeCallback={ ( newValue ) => {
|
|
||||||
updateButtonStyle( 'tagline', newValue );
|
|
||||||
} }
|
|
||||||
currentValue={ locationSettings.settings.style.tagline }
|
|
||||||
/>
|
|
||||||
</TabStylingSection>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SectionButtonPreview = ( { locationSettings } ) => {
|
|
||||||
return (
|
|
||||||
<PayPalScriptProvider
|
|
||||||
options={ {
|
|
||||||
clientId: 'test',
|
|
||||||
merchantId: 'QTQX5NP6N9WZU',
|
|
||||||
components: 'buttons,googlepay',
|
|
||||||
'disable-funding': 'card',
|
|
||||||
'buyer-country': 'US',
|
|
||||||
currency: 'USD',
|
|
||||||
} }
|
|
||||||
>
|
|
||||||
<PayPalButtons
|
|
||||||
style={ locationSettings.settings.style }
|
|
||||||
forceReRender={ [ locationSettings.settings.style ] }
|
|
||||||
>
|
|
||||||
Error
|
|
||||||
</PayPalButtons>
|
|
||||||
</PayPalScriptProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TabStyling;
|
|
|
@ -1,16 +1,34 @@
|
||||||
import { Button } from '@wordpress/components';
|
import { Button } from '@wordpress/components';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
|
import { CommonHooks, StylingHooks } from '../../../../data';
|
||||||
import TopNavigation from '../../../ReusableComponents/TopNavigation';
|
import TopNavigation from '../../../ReusableComponents/TopNavigation';
|
||||||
|
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
|
||||||
|
|
||||||
const SettingsNavigation = () => {
|
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' );
|
const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopNavigation title={ title } exitOnTitleClick={ true }>
|
<TopNavigation title={ title } exitOnTitleClick={ true }>
|
||||||
<Button variant="primary" disabled={ false }>
|
<BusyStateWrapper>
|
||||||
{ __( 'Save', 'woocommerce-paypal-payments' ) }
|
<Button variant="primary" onClick={ handleSaveClick }>
|
||||||
</Button>
|
{ __( 'Save', 'woocommerce-paypal-payments' ) }
|
||||||
|
</Button>
|
||||||
|
</BusyStateWrapper>
|
||||||
</TopNavigation>
|
</TopNavigation>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 (
|
||||||
|
<SelectStylingSection
|
||||||
|
title={ __( 'Button Color', 'woocommerce-paypal-payments' ) }
|
||||||
|
className="button-color"
|
||||||
|
options={ choices }
|
||||||
|
value={ color }
|
||||||
|
onChange={ setColor }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionButtonColor;
|
|
@ -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 (
|
||||||
|
<SelectStylingSection
|
||||||
|
title={ __( 'Button Label', 'woocommerce-paypal-payments' ) }
|
||||||
|
className="button-label"
|
||||||
|
options={ choices }
|
||||||
|
value={ label }
|
||||||
|
onChange={ setLabel }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionButtonLabel;
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<RadiobuttonStylingSection
|
||||||
|
className="button-layout"
|
||||||
|
title={ __( 'Button Layout', 'woocommerce-paypal-payments' ) }
|
||||||
|
options={ choices }
|
||||||
|
selected={ layout }
|
||||||
|
onChange={ setLayout }
|
||||||
|
/>
|
||||||
|
<Tagline location={ location } />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionButtonLayout;
|
|
@ -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 (
|
||||||
|
<RadiobuttonStylingSection
|
||||||
|
title={ __( 'Shape', 'woocommerce-paypal-payments' ) }
|
||||||
|
className="button-shape"
|
||||||
|
options={ choices }
|
||||||
|
selected={ shape }
|
||||||
|
onChange={ setShape }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionButtonShape;
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<StylingSection
|
||||||
|
className="header-section"
|
||||||
|
bigTitle={ true }
|
||||||
|
title={ __( 'Button Styling', 'wooocommerce-paypal-payments' ) }
|
||||||
|
description={ __(
|
||||||
|
'Customize the appearance of the PayPal smart buttons on your website and choose which payment buttons to display.',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
) }
|
||||||
|
/>
|
||||||
|
<SelectStylingSection
|
||||||
|
className="location-selector"
|
||||||
|
title={ __( 'Location', 'woocommerce-paypal-payments' ) }
|
||||||
|
separatorAndGap={ false }
|
||||||
|
options={ choices }
|
||||||
|
value={ location }
|
||||||
|
onChange={ setLocation }
|
||||||
|
>
|
||||||
|
{ details.link && (
|
||||||
|
<Button
|
||||||
|
icon={ help }
|
||||||
|
href={ details.link }
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</SelectStylingSection>
|
||||||
|
<CheckboxStylingSection
|
||||||
|
className="location-activation"
|
||||||
|
separatorAndGap={ false }
|
||||||
|
options={ [ activateCheckbox ] }
|
||||||
|
value={ isActive }
|
||||||
|
onChange={ setActive }
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LocationSelector;
|
|
@ -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 (
|
||||||
|
<CheckboxStylingSection
|
||||||
|
title={ __( 'Payment Methods', 'woocommerce-paypal-payments' ) }
|
||||||
|
className="payment-methods"
|
||||||
|
options={ choices }
|
||||||
|
value={ paymentMethods }
|
||||||
|
onChange={ setPaymentMethods }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionPaymentMethods;
|
|
@ -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 (
|
||||||
|
<CheckboxStylingSection
|
||||||
|
className="tagline"
|
||||||
|
separatorAndGap={ false }
|
||||||
|
options={ [ checkbox ] }
|
||||||
|
value={ tagline }
|
||||||
|
onChange={ setTagline }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionTagline;
|
|
@ -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';
|
|
@ -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 (
|
||||||
|
<SettingsBlock
|
||||||
|
className={ className }
|
||||||
|
separatorAndGap={ separatorAndGap }
|
||||||
|
>
|
||||||
|
<Header>
|
||||||
|
<Title altStyle={ true } big={ bigTitle }>
|
||||||
|
{ title }
|
||||||
|
</Title>
|
||||||
|
<Description>{ description }</Description>
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<Content className="section-content">{ children }</Content>
|
||||||
|
</SettingsBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StylingSection;
|
|
@ -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 (
|
||||||
|
<StylingSection
|
||||||
|
title={ title }
|
||||||
|
className={ className }
|
||||||
|
description={ description }
|
||||||
|
separatorAndGap={ separatorAndGap }
|
||||||
|
>
|
||||||
|
<HStack spacing={ 6 }>
|
||||||
|
<CheckboxGroup
|
||||||
|
options={ options }
|
||||||
|
value={ value }
|
||||||
|
onChange={ onChange }
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{ children }
|
||||||
|
</StylingSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StylingSectionWithCheckboxes;
|
|
@ -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 (
|
||||||
|
<StylingSection
|
||||||
|
title={ title }
|
||||||
|
className={ className }
|
||||||
|
description={ description }
|
||||||
|
separatorAndGap={ separatorAndGap }
|
||||||
|
>
|
||||||
|
<HStack>
|
||||||
|
<RadioControl
|
||||||
|
options={ options }
|
||||||
|
selected={ selected }
|
||||||
|
onChange={ onChange }
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{ children }
|
||||||
|
</StylingSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StylingSectionWithRadiobuttons;
|
|
@ -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 (
|
||||||
|
<StylingSection
|
||||||
|
title={ title }
|
||||||
|
className={ className }
|
||||||
|
description={ description }
|
||||||
|
separatorAndGap={ separatorAndGap }
|
||||||
|
>
|
||||||
|
<SelectControl
|
||||||
|
__nextHasNoMarginBottom
|
||||||
|
options={ options }
|
||||||
|
value={ value }
|
||||||
|
onChange={ onChange }
|
||||||
|
/>
|
||||||
|
|
||||||
|
{ children }
|
||||||
|
</StylingSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StylingSectionWithSelect;
|
|
@ -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';
|
|
@ -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 (
|
||||||
|
<div className="preview-panel">
|
||||||
|
<div className="preview-panel-inner">
|
||||||
|
<PayPalScriptProvider options={ providerOptions }>
|
||||||
|
<PayPalButtons style={ style } forceReRender={ [ style ] }>
|
||||||
|
Error
|
||||||
|
</PayPalButtons>
|
||||||
|
</PayPalScriptProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PreviewPanel;
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<PaymentMethods location={ location } />
|
||||||
|
<ButtonLayout location={ location } />
|
||||||
|
<ButtonShape location={ location } />
|
||||||
|
<ButtonLabel location={ location } />
|
||||||
|
<ButtonColor location={ location } />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="settings-panel">
|
||||||
|
<LocationSelector
|
||||||
|
location={ location }
|
||||||
|
setLocation={ setLocation }
|
||||||
|
/>
|
||||||
|
<LocationDetails />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsPanel;
|
|
@ -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 (
|
||||||
|
<div className="ppcp-r-styling">
|
||||||
|
<SettingsPanel location={ location } setLocation={ setLocation } />
|
||||||
|
<PreviewPanel location={ location } />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TabStyling;
|
|
@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n';
|
||||||
import TabOverview from '../../Overview/TabOverview';
|
import TabOverview from '../../Overview/TabOverview';
|
||||||
import TabPaymentMethods from '../../Overview/TabPaymentMethods';
|
import TabPaymentMethods from '../../Overview/TabPaymentMethods';
|
||||||
import TabSettings from '../../Overview/TabSettings';
|
import TabSettings from '../../Overview/TabSettings';
|
||||||
import TabStyling from '../../Overview/TabStyling';
|
import TabStyling from './TabStyling';
|
||||||
import TabPayLaterMessaging from '../../Overview/TabPayLaterMessaging';
|
import TabPayLaterMessaging from '../../Overview/TabPayLaterMessaging';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -36,28 +36,37 @@ export const hydrate = ( payload ) => ( {
|
||||||
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.
|
* Transient. Marks the store as "ready", i.e., fully initialized.
|
||||||
*
|
*
|
||||||
* @param {boolean} isReady
|
* @param {boolean} isReady
|
||||||
* @return {Action} The action.
|
* @return {Action} The action.
|
||||||
*/
|
*/
|
||||||
export const setIsReady = ( isReady ) => ( {
|
export const setIsReady = ( isReady ) => setTransient( 'isReady', 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 },
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Side effect. Triggers the persistence of store data to the server.
|
* Side effect. Triggers the persistence of store data to the server.
|
||||||
|
|
|
@ -7,39 +7,24 @@
|
||||||
* @file
|
* @file
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useSelect, useDispatch } from '@wordpress/data';
|
import { useDispatch } from '@wordpress/data';
|
||||||
|
|
||||||
|
import { createHooksForStore } from '../utils';
|
||||||
import { STORE_NAME } from './constants';
|
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 useHooks = () => {
|
||||||
const {
|
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
|
||||||
persist,
|
const { persist } = useDispatch( STORE_NAME );
|
||||||
|
|
||||||
// TODO: Replace with real property.
|
|
||||||
setSampleValue,
|
|
||||||
} = useDispatch( STORE_NAME );
|
|
||||||
|
|
||||||
// Read-only flags and derived state.
|
// Read-only flags and derived state.
|
||||||
// Nothing here yet.
|
// Nothing here yet.
|
||||||
|
|
||||||
// Transient accessors.
|
// Transient accessors.
|
||||||
const isReady = useTransient( 'isReady' );
|
const [ isReady ] = useTransient( 'isReady' );
|
||||||
|
|
||||||
// Persistent accessors.
|
// Persistent accessors.
|
||||||
// TODO: Replace with real property.
|
// TODO: Replace with real property.
|
||||||
const sampleValue = usePersistent( 'sampleValue' );
|
const [ sampleValue, setSampleValue ] = usePersistent( 'sampleValue' );
|
||||||
|
|
||||||
return {
|
return {
|
||||||
persist,
|
persist,
|
||||||
|
|
|
@ -36,16 +36,37 @@ export const hydrate = ( payload ) => ( {
|
||||||
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.
|
* Transient. Marks the onboarding details as "ready", i.e., fully initialized.
|
||||||
*
|
*
|
||||||
* @param {boolean} isReady
|
* @param {boolean} isReady
|
||||||
* @return {Action} The action.
|
* @return {Action} The action.
|
||||||
*/
|
*/
|
||||||
export const setIsReady = ( isReady ) => ( {
|
export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
|
||||||
type: ACTION_TYPES.SET_TRANSIENT,
|
|
||||||
payload: { isReady },
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transient. Sets the active settings tab.
|
* Transient. Sets the active settings tab.
|
||||||
|
@ -53,21 +74,8 @@ export const setIsReady = ( isReady ) => ( {
|
||||||
* @param {string} activeModal
|
* @param {string} activeModal
|
||||||
* @return {Action} The action.
|
* @return {Action} The action.
|
||||||
*/
|
*/
|
||||||
export const setActiveModal = ( activeModal ) => ( {
|
export const setActiveModal = ( activeModal ) =>
|
||||||
type: ACTION_TYPES.SET_TRANSIENT,
|
setTransient( 'activeModal', activeModal );
|
||||||
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 },
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transient (Activity): Marks the start of an async activity
|
* Transient (Activity): Marks the start of an async activity
|
||||||
|
@ -107,10 +115,8 @@ export const stopActivity = ( id ) => ( {
|
||||||
* @param {boolean} useSandbox
|
* @param {boolean} useSandbox
|
||||||
* @return {Action} The action.
|
* @return {Action} The action.
|
||||||
*/
|
*/
|
||||||
export const setSandboxMode = ( useSandbox ) => ( {
|
export const setSandboxMode = ( useSandbox ) =>
|
||||||
type: ACTION_TYPES.SET_PERSISTENT,
|
setPersistent( 'useSandbox', useSandbox );
|
||||||
payload: { useSandbox },
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persistent. Toggles the "Manual Connection" mode on or off.
|
* Persistent. Toggles the "Manual Connection" mode on or off.
|
||||||
|
@ -118,10 +124,8 @@ export const setSandboxMode = ( useSandbox ) => ( {
|
||||||
* @param {boolean} useManualConnection
|
* @param {boolean} useManualConnection
|
||||||
* @return {Action} The action.
|
* @return {Action} The action.
|
||||||
*/
|
*/
|
||||||
export const setManualConnectionMode = ( useManualConnection ) => ( {
|
export const setManualConnectionMode = ( useManualConnection ) =>
|
||||||
type: ACTION_TYPES.SET_PERSISTENT,
|
setPersistent( 'useManualConnection', useManualConnection );
|
||||||
payload: { useManualConnection },
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Side effect. Saves the persistent details to the WP database.
|
* Side effect. Saves the persistent details to the WP database.
|
||||||
|
|
|
@ -9,41 +9,31 @@
|
||||||
|
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
import { useCallback } from '@wordpress/element';
|
import { useCallback } from '@wordpress/element';
|
||||||
|
|
||||||
|
import { createHooksForStore } from '../utils';
|
||||||
import { STORE_NAME } from './constants';
|
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 useHooks = () => {
|
||||||
|
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
|
||||||
const {
|
const {
|
||||||
persist,
|
persist,
|
||||||
setSandboxMode,
|
|
||||||
setManualConnectionMode,
|
|
||||||
sandboxOnboardingUrl,
|
sandboxOnboardingUrl,
|
||||||
productionOnboardingUrl,
|
productionOnboardingUrl,
|
||||||
authenticateWithCredentials,
|
authenticateWithCredentials,
|
||||||
authenticateWithOAuth,
|
authenticateWithOAuth,
|
||||||
setActiveModal,
|
|
||||||
startWebhookSimulation,
|
startWebhookSimulation,
|
||||||
checkWebhookSimulationState,
|
checkWebhookSimulationState,
|
||||||
} = useDispatch( STORE_NAME );
|
} = useDispatch( STORE_NAME );
|
||||||
|
|
||||||
// Transient accessors.
|
// Transient accessors.
|
||||||
const isReady = useTransient( 'isReady' );
|
const [ isReady ] = useTransient( 'isReady' );
|
||||||
const activeModal = useTransient( 'activeModal' );
|
const [ activeModal, setActiveModal ] = useTransient( 'activeModal' );
|
||||||
|
|
||||||
// Persistent accessors.
|
// Persistent accessors.
|
||||||
const isSandboxMode = usePersistent( 'useSandbox' );
|
const [ isSandboxMode, setSandboxMode ] = usePersistent( 'useSandbox' );
|
||||||
const isManualConnectionMode = usePersistent( 'useManualConnection' );
|
const [ isManualConnectionMode, setManualConnectionMode ] = usePersistent(
|
||||||
|
'useManualConnection'
|
||||||
|
);
|
||||||
const merchant = useSelect(
|
const merchant = useSelect(
|
||||||
( select ) => select( STORE_NAME ).merchant(),
|
( select ) => select( STORE_NAME ).merchant(),
|
||||||
[]
|
[]
|
||||||
|
|
10
modules/ppcp-settings/resources/js/data/configuration.js
Normal file
10
modules/ppcp-settings/resources/js/data/configuration.js
Normal file
|
@ -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';
|
|
@ -1,16 +1,20 @@
|
||||||
import { addDebugTools } from './debug';
|
import { addDebugTools } from './debug';
|
||||||
import * as Onboarding from './onboarding';
|
import * as Onboarding from './onboarding';
|
||||||
import * as Common from './common';
|
import * as Common from './common';
|
||||||
|
import * as Styling from './styling';
|
||||||
|
|
||||||
Onboarding.initStore();
|
Onboarding.initStore();
|
||||||
Common.initStore();
|
Common.initStore();
|
||||||
|
Styling.initStore();
|
||||||
|
|
||||||
export const OnboardingHooks = Onboarding.hooks;
|
export const OnboardingHooks = Onboarding.hooks;
|
||||||
export const CommonHooks = Common.hooks;
|
export const CommonHooks = Common.hooks;
|
||||||
|
export const StylingHooks = Styling.hooks;
|
||||||
|
|
||||||
export const OnboardingStoreName = Onboarding.STORE_NAME;
|
export const OnboardingStoreName = Onboarding.STORE_NAME;
|
||||||
export const CommonStoreName = Common.STORE_NAME;
|
export const CommonStoreName = Common.STORE_NAME;
|
||||||
|
export const StylingStoreName = Styling.STORE_NAME;
|
||||||
|
|
||||||
export * from './constants';
|
export * from './configuration';
|
||||||
|
|
||||||
addDebugTools( window.ppcpSettings, [ Onboarding, Common ] );
|
addDebugTools( window.ppcpSettings, [ Onboarding, Common, Styling ] );
|
||||||
|
|
|
@ -36,16 +36,37 @@ export const hydrate = ( payload ) => ( {
|
||||||
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.
|
* Transient. Marks the onboarding details as "ready", i.e., fully initialized.
|
||||||
*
|
*
|
||||||
* @param {boolean} isReady
|
* @param {boolean} isReady
|
||||||
* @return {Action} The action.
|
* @return {Action} The action.
|
||||||
*/
|
*/
|
||||||
export const setIsReady = ( isReady ) => ( {
|
export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
|
||||||
type: ACTION_TYPES.SET_TRANSIENT,
|
|
||||||
payload: { isReady },
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transient. Sets the "manualClientId" value.
|
* Transient. Sets the "manualClientId" value.
|
||||||
|
@ -53,10 +74,8 @@ export const setIsReady = ( isReady ) => ( {
|
||||||
* @param {string} manualClientId
|
* @param {string} manualClientId
|
||||||
* @return {Action} The action.
|
* @return {Action} The action.
|
||||||
*/
|
*/
|
||||||
export const setManualClientId = ( manualClientId ) => ( {
|
export const setManualClientId = ( manualClientId ) =>
|
||||||
type: ACTION_TYPES.SET_TRANSIENT,
|
setTransient( 'manualClientId', manualClientId );
|
||||||
payload: { manualClientId },
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transient. Sets the "manualClientSecret" value.
|
* Transient. Sets the "manualClientSecret" value.
|
||||||
|
@ -64,10 +83,8 @@ export const setManualClientId = ( manualClientId ) => ( {
|
||||||
* @param {string} manualClientSecret
|
* @param {string} manualClientSecret
|
||||||
* @return {Action} The action.
|
* @return {Action} The action.
|
||||||
*/
|
*/
|
||||||
export const setManualClientSecret = ( manualClientSecret ) => ( {
|
export const setManualClientSecret = ( manualClientSecret ) =>
|
||||||
type: ACTION_TYPES.SET_TRANSIENT,
|
setTransient( 'manualClientSecret', manualClientSecret );
|
||||||
payload: { manualClientSecret },
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persistent.Set the "onboarding completed" flag which shows or hides the wizard.
|
* Persistent.Set the "onboarding completed" flag which shows or hides the wizard.
|
||||||
|
@ -75,10 +92,8 @@ export const setManualClientSecret = ( manualClientSecret ) => ( {
|
||||||
* @param {boolean} completed
|
* @param {boolean} completed
|
||||||
* @return {Action} The action.
|
* @return {Action} The action.
|
||||||
*/
|
*/
|
||||||
export const setCompleted = ( completed ) => ( {
|
export const setCompleted = ( completed ) =>
|
||||||
type: ACTION_TYPES.SET_PERSISTENT,
|
setPersistent( 'completed', completed );
|
||||||
payload: { completed },
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persistent. Sets the onboarding wizard to a new step.
|
* Persistent. Sets the onboarding wizard to a new step.
|
||||||
|
@ -86,10 +101,7 @@ export const setCompleted = ( completed ) => ( {
|
||||||
* @param {number} step
|
* @param {number} step
|
||||||
* @return {Action} The action.
|
* @return {Action} The action.
|
||||||
*/
|
*/
|
||||||
export const setStep = ( step ) => ( {
|
export const setStep = ( step ) => setPersistent( 'step', step );
|
||||||
type: ACTION_TYPES.SET_PERSISTENT,
|
|
||||||
payload: { step },
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persistent. Sets the "isCasualSeller" value.
|
* Persistent. Sets the "isCasualSeller" value.
|
||||||
|
@ -97,10 +109,8 @@ export const setStep = ( step ) => ( {
|
||||||
* @param {boolean} isCasualSeller
|
* @param {boolean} isCasualSeller
|
||||||
* @return {Action} The action.
|
* @return {Action} The action.
|
||||||
*/
|
*/
|
||||||
export const setIsCasualSeller = ( isCasualSeller ) => ( {
|
export const setIsCasualSeller = ( isCasualSeller ) =>
|
||||||
type: ACTION_TYPES.SET_PERSISTENT,
|
setPersistent( 'isCasualSeller', isCasualSeller );
|
||||||
payload: { isCasualSeller },
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persistent. Sets the "areOptionalPaymentMethodsEnabled" value.
|
* Persistent. Sets the "areOptionalPaymentMethodsEnabled" value.
|
||||||
|
@ -110,10 +120,11 @@ export const setIsCasualSeller = ( isCasualSeller ) => ( {
|
||||||
*/
|
*/
|
||||||
export const setAreOptionalPaymentMethodsEnabled = (
|
export const setAreOptionalPaymentMethodsEnabled = (
|
||||||
areOptionalPaymentMethodsEnabled
|
areOptionalPaymentMethodsEnabled
|
||||||
) => ( {
|
) =>
|
||||||
type: ACTION_TYPES.SET_PERSISTENT,
|
setPersistent(
|
||||||
payload: { areOptionalPaymentMethodsEnabled },
|
'areOptionalPaymentMethodsEnabled',
|
||||||
} );
|
areOptionalPaymentMethodsEnabled
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persistent. Sets the "products" array.
|
* Persistent. Sets the "products" array.
|
||||||
|
@ -121,10 +132,8 @@ export const setAreOptionalPaymentMethodsEnabled = (
|
||||||
* @param {string[]} products
|
* @param {string[]} products
|
||||||
* @return {Action} The action.
|
* @return {Action} The action.
|
||||||
*/
|
*/
|
||||||
export const setProducts = ( products ) => ( {
|
export const setProducts = ( products ) =>
|
||||||
type: ACTION_TYPES.SET_PERSISTENT,
|
setPersistent( 'products', products );
|
||||||
payload: { products },
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Side effect. Triggers the persistence of onboarding data to the server.
|
* Side effect. Triggers the persistence of onboarding data to the server.
|
||||||
|
|
|
@ -1,8 +1,24 @@
|
||||||
|
/**
|
||||||
|
* Configuration for UI components.
|
||||||
|
*
|
||||||
|
* @file
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Onboarding options for StepBusiness
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
export const BUSINESS_TYPES = {
|
export const BUSINESS_TYPES = {
|
||||||
CASUAL_SELLER: 'casual_seller',
|
CASUAL_SELLER: 'casual_seller',
|
||||||
BUSINESS: 'business',
|
BUSINESS: 'business',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Onboarding options for StepProducts
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
export const PRODUCT_TYPES = {
|
export const PRODUCT_TYPES = {
|
||||||
VIRTUAL: 'virtual',
|
VIRTUAL: 'virtual',
|
||||||
PHYSICAL: 'physical',
|
PHYSICAL: 'physical',
|
|
@ -8,7 +8,7 @@
|
||||||
export const STORE_NAME = 'wc/paypal/onboarding';
|
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
|
* Used by: Resolvers
|
||||||
* See: OnboardingRestEndpoint.php
|
* See: OnboardingRestEndpoint.php
|
||||||
|
|
|
@ -9,32 +9,14 @@
|
||||||
|
|
||||||
import { useSelect, useDispatch } from '@wordpress/data';
|
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';
|
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 useHooks = () => {
|
||||||
const {
|
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
|
||||||
persist,
|
|
||||||
setStep,
|
const { persist } = useDispatch( STORE_NAME );
|
||||||
setCompleted,
|
|
||||||
setIsCasualSeller,
|
|
||||||
setManualClientId,
|
|
||||||
setManualClientSecret,
|
|
||||||
setAreOptionalPaymentMethodsEnabled,
|
|
||||||
setProducts,
|
|
||||||
} = useDispatch( STORE_NAME );
|
|
||||||
|
|
||||||
// Read-only flags and derived state.
|
// Read-only flags and derived state.
|
||||||
const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] );
|
const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] );
|
||||||
|
@ -44,18 +26,22 @@ const useHooks = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Transient accessors.
|
// Transient accessors.
|
||||||
const isReady = useTransient( 'isReady' );
|
const [ isReady ] = useTransient( 'isReady' );
|
||||||
const manualClientId = useTransient( 'manualClientId' );
|
const [ manualClientId, setManualClientId ] =
|
||||||
const manualClientSecret = useTransient( 'manualClientSecret' );
|
useTransient( 'manualClientId' );
|
||||||
|
const [ manualClientSecret, setManualClientSecret ] =
|
||||||
|
useTransient( 'manualClientSecret' );
|
||||||
|
|
||||||
// Persistent accessors.
|
// Persistent accessors.
|
||||||
const step = usePersistent( 'step' );
|
const [ step, setStep ] = usePersistent( 'step' );
|
||||||
const completed = usePersistent( 'completed' );
|
const [ completed, setCompleted ] = usePersistent( 'completed' );
|
||||||
const isCasualSeller = usePersistent( 'isCasualSeller' );
|
const [ isCasualSeller, setIsCasualSeller ] =
|
||||||
const areOptionalPaymentMethodsEnabled = usePersistent(
|
usePersistent( 'isCasualSeller' );
|
||||||
'areOptionalPaymentMethodsEnabled'
|
const [
|
||||||
);
|
areOptionalPaymentMethodsEnabled,
|
||||||
const products = usePersistent( 'products' );
|
setAreOptionalPaymentMethodsEnabled,
|
||||||
|
] = usePersistent( 'areOptionalPaymentMethodsEnabled' );
|
||||||
|
const [ products, setProducts ] = usePersistent( 'products' );
|
||||||
|
|
||||||
const savePersistent = async ( setter, value ) => {
|
const savePersistent = async ( setter, value ) => {
|
||||||
setter( value );
|
setter( value );
|
||||||
|
|
|
@ -1,162 +0,0 @@
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
|
|
||||||
const cartAndExpressCheckoutSettings = {
|
|
||||||
paymentMethods: [],
|
|
||||||
style: {
|
|
||||||
shape: 'pill',
|
|
||||||
label: 'paypal',
|
|
||||||
color: 'gold',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const settings = {
|
|
||||||
paymentMethods: [],
|
|
||||||
style: {
|
|
||||||
layout: 'vertical',
|
|
||||||
shape: cartAndExpressCheckoutSettings.style.shape,
|
|
||||||
label: cartAndExpressCheckoutSettings.style.label,
|
|
||||||
color: cartAndExpressCheckoutSettings.style.color,
|
|
||||||
tagline: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultLocationSettings = {
|
|
||||||
cart: {
|
|
||||||
value: 'cart',
|
|
||||||
label: __( 'Cart', 'woocommerce-paypal-payments' ),
|
|
||||||
settings: { ...cartAndExpressCheckoutSettings },
|
|
||||||
// translators: %s: Link to Cart page
|
|
||||||
description: __(
|
|
||||||
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Cart page</a> and select which additional payment buttons to display in this location.',
|
|
||||||
'wooocommerce-paypal-payments'
|
|
||||||
),
|
|
||||||
descriptionLink: '#',
|
|
||||||
},
|
|
||||||
'classic-checkout': {
|
|
||||||
value: 'classic-checkout',
|
|
||||||
label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ),
|
|
||||||
settings: { ...settings },
|
|
||||||
// translators: %s: Link to Classic Checkout page
|
|
||||||
description: __(
|
|
||||||
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Classic Checkout page</a> and choose which additional payment buttons to display in this location.',
|
|
||||||
'wooocommerce-paypal-payments'
|
|
||||||
),
|
|
||||||
descriptionLink: '#',
|
|
||||||
},
|
|
||||||
'express-checkout': {
|
|
||||||
value: 'express-checkout',
|
|
||||||
label: __( 'Express Checkout', 'woocommerce-paypal-payments' ),
|
|
||||||
settings: { ...cartAndExpressCheckoutSettings },
|
|
||||||
// translators: %s: Link to Express Checkout location
|
|
||||||
description: __(
|
|
||||||
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Express Checkout location</a> and choose which additional payment buttons to display in this location.',
|
|
||||||
'wooocommerce-paypal-payments'
|
|
||||||
),
|
|
||||||
descriptionLink: '#',
|
|
||||||
},
|
|
||||||
'mini-cart': {
|
|
||||||
value: 'mini-cart',
|
|
||||||
label: __( 'Mini Cart', 'woocommerce-paypel-payements' ),
|
|
||||||
settings: { ...settings },
|
|
||||||
// translators: %s: Link to Mini Cart
|
|
||||||
description: __(
|
|
||||||
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Mini Cart</a> and choose which additional payment buttons to display in this location.',
|
|
||||||
'wooocommerce-paypal-payments'
|
|
||||||
),
|
|
||||||
descriptionLink: '#',
|
|
||||||
},
|
|
||||||
'product-page': {
|
|
||||||
value: 'product-page',
|
|
||||||
label: __( 'Product Page', 'woocommerce-paypal-payments' ),
|
|
||||||
settings: { ...settings },
|
|
||||||
// translators: %s: Link to Product Page
|
|
||||||
description: __(
|
|
||||||
'Customize the appearance of the PayPal smart buttons on the <a href="%s">[MISSING LINK]Product Page</a> and choose which additional payment buttons to display in this location.',
|
|
||||||
'wooocommerce-paypal-payments'
|
|
||||||
),
|
|
||||||
descriptionLink: '#',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const paymentMethodOptions = [
|
|
||||||
{
|
|
||||||
value: 'venmo',
|
|
||||||
label: __( 'Venmo', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'paylater',
|
|
||||||
label: __( 'Pay Later', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'googlepay',
|
|
||||||
label: __( 'Google Pay', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'applepay',
|
|
||||||
label: __( 'Apple Pay', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const buttonLabelOptions = [
|
|
||||||
{
|
|
||||||
value: 'paypal',
|
|
||||||
label: __( 'PayPal', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'checkout',
|
|
||||||
label: __( 'Checkout', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'buynow',
|
|
||||||
label: __( 'PayPal Buy Now', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'pay',
|
|
||||||
label: __( 'Pay with PayPal', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const colorOptions = [
|
|
||||||
{
|
|
||||||
value: 'gold',
|
|
||||||
label: __( 'Gold (Recommended)', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'blue',
|
|
||||||
label: __( 'Blue', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'silver',
|
|
||||||
label: __( 'Silver', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'black',
|
|
||||||
label: __( 'Black', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'white',
|
|
||||||
label: __( 'White', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const buttonLayoutOptions = [
|
|
||||||
{
|
|
||||||
label: __( 'Vertical', 'woocommerce-paypal-payments' ),
|
|
||||||
value: 'vertical',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: __( 'Horizontal', 'woocommerce-paypal-payments' ),
|
|
||||||
value: 'horizontal',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const shapeOptions = [
|
|
||||||
{
|
|
||||||
value: 'pill',
|
|
||||||
label: __( 'Pill', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'rect',
|
|
||||||
label: __( 'Rectangle', 'woocommerce-paypal-payments' ),
|
|
||||||
},
|
|
||||||
];
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* Action Types: Define unique identifiers for actions across all store modules.
|
||||||
|
*
|
||||||
|
* @file
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// Transient data.
|
||||||
|
SET_TRANSIENT: 'STYLE:SET_TRANSIENT',
|
||||||
|
|
||||||
|
// Persistent data.
|
||||||
|
SET_PERSISTENT: 'STYLE:SET_PERSISTENT',
|
||||||
|
RESET: 'STYLE:RESET',
|
||||||
|
HYDRATE: 'STYLE:HYDRATE',
|
||||||
|
|
||||||
|
// Controls - always start with "DO_".
|
||||||
|
DO_PERSIST_DATA: 'STYLE:DO_PERSIST_DATA',
|
||||||
|
};
|
80
modules/ppcp-settings/resources/js/data/styling/actions.js
Normal file
80
modules/ppcp-settings/resources/js/data/styling/actions.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/**
|
||||||
|
* 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. Set the full store details during app initialization.
|
||||||
|
*
|
||||||
|
* @param {{data: {}, flags?: {}}} payload
|
||||||
|
* @return {Action} The action.
|
||||||
|
*/
|
||||||
|
export const hydrate = ( payload ) => ( {
|
||||||
|
type: ACTION_TYPES.HYDRATE,
|
||||||
|
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. Changes the "ready-state" of the module.
|
||||||
|
*
|
||||||
|
* @param {boolean} state Whether the store is ready to be used.
|
||||||
|
* @return {Action} The action.
|
||||||
|
*/
|
||||||
|
export const setIsReady = ( state ) => setTransient( 'isReady', state );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Side effect. Triggers the persistence of store data to the server.
|
||||||
|
*
|
||||||
|
* @return {Action} The action.
|
||||||
|
*/
|
||||||
|
export const persist = function* () {
|
||||||
|
const data = yield select( STORE_NAME ).persistentData();
|
||||||
|
|
||||||
|
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
|
||||||
|
};
|
131
modules/ppcp-settings/resources/js/data/styling/configuration.js
Normal file
131
modules/ppcp-settings/resources/js/data/styling/configuration.js
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
/**
|
||||||
|
* Configuration for UI components.
|
||||||
|
*
|
||||||
|
* @file
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
|
export const STYLING_LOCATIONS = {
|
||||||
|
cart: {
|
||||||
|
value: 'cart',
|
||||||
|
label: __( 'Cart', 'woocommerce-paypal-payments' ),
|
||||||
|
link: 'https://woocommerce.com/document/woocommerce-paypal-payments/#button-on-cart',
|
||||||
|
props: { layout: false, tagline: false },
|
||||||
|
},
|
||||||
|
classicCheckout: {
|
||||||
|
value: 'classicCheckout',
|
||||||
|
label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ),
|
||||||
|
link: 'https://woocommerce.com/document/woocommerce-paypal-payments/#button-on-checkout',
|
||||||
|
props: { layout: true, tagline: true },
|
||||||
|
},
|
||||||
|
expressCheckout: {
|
||||||
|
value: 'expressCheckout',
|
||||||
|
label: __( 'Express Checkout', 'woocommerce-paypal-payments' ),
|
||||||
|
link: 'https://woocommerce.com/document/woocommerce-paypal-payments/#button-on-block-express-checkout',
|
||||||
|
props: { layout: false, tagline: false },
|
||||||
|
},
|
||||||
|
miniCart: {
|
||||||
|
value: 'miniCart',
|
||||||
|
label: __( 'Mini Cart', 'woocommerce-paypel-payements' ),
|
||||||
|
link: 'https://woocommerce.com/document/woocommerce-paypal-payments/#button-on-mini-cart',
|
||||||
|
props: { layout: true, tagline: true },
|
||||||
|
},
|
||||||
|
product: {
|
||||||
|
value: 'product',
|
||||||
|
label: __( 'Product Page', 'woocommerce-paypal-payments' ),
|
||||||
|
link: 'https://woocommerce.com/document/woocommerce-paypal-payments/#button-on-single-product',
|
||||||
|
props: { layout: true, tagline: true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const STYLING_LABELS = {
|
||||||
|
paypal: {
|
||||||
|
value: 'paypal',
|
||||||
|
label: __( 'PayPal', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
checkout: {
|
||||||
|
value: 'checkout',
|
||||||
|
label: __( 'Checkout', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
buynow: {
|
||||||
|
value: 'buynow',
|
||||||
|
label: __( 'PayPal Buy Now', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
pay: {
|
||||||
|
value: 'pay',
|
||||||
|
label: __( 'Pay with PayPal', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const STYLING_COLORS = {
|
||||||
|
gold: {
|
||||||
|
value: 'gold',
|
||||||
|
label: __( 'Gold (Recommended)', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
value: 'blue',
|
||||||
|
label: __( 'Blue', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
silver: {
|
||||||
|
value: 'silver',
|
||||||
|
label: __( 'Silver', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
black: {
|
||||||
|
value: 'black',
|
||||||
|
label: __( 'Black', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
white: {
|
||||||
|
value: 'white',
|
||||||
|
label: __( 'White', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const STYLING_LAYOUTS = {
|
||||||
|
vertical: {
|
||||||
|
value: 'vertical',
|
||||||
|
label: __( 'Vertical', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
horizontal: {
|
||||||
|
value: 'horizontal',
|
||||||
|
label: __( 'Horizontal', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const STYLING_SHAPES = {
|
||||||
|
rect: {
|
||||||
|
value: 'rect',
|
||||||
|
label: __( 'Rectangle', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
pill: {
|
||||||
|
value: 'pill',
|
||||||
|
label: __( 'Pill', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const STYLING_PAYMENT_METHODS = {
|
||||||
|
paypal: {
|
||||||
|
value: '',
|
||||||
|
label: __( 'PayPal', 'woocommerce-paypal-payments' ),
|
||||||
|
checked: true,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
venmo: {
|
||||||
|
value: 'venmo',
|
||||||
|
label: __( 'Venmo', 'woocommerce-paypal-payments' ),
|
||||||
|
isFunding: true,
|
||||||
|
},
|
||||||
|
paylater: {
|
||||||
|
value: 'paylater',
|
||||||
|
label: __( 'Pay Later', 'woocommerce-paypal-payments' ),
|
||||||
|
isFunding: true,
|
||||||
|
},
|
||||||
|
googlepay: {
|
||||||
|
value: 'googlepay',
|
||||||
|
label: __( 'Google Pay', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
applepay: {
|
||||||
|
value: 'applepay',
|
||||||
|
label: __( 'Apple Pay', 'woocommerce-paypal-payments' ),
|
||||||
|
},
|
||||||
|
};
|
28
modules/ppcp-settings/resources/js/data/styling/constants.js
Normal file
28
modules/ppcp-settings/resources/js/data/styling/constants.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* Name of the Redux store module.
|
||||||
|
*
|
||||||
|
* Used by: Reducer, Selector, Index
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
export const STORE_NAME = 'wc/paypal/style';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST path to hydrate data of this module by loading data from the WP DB.
|
||||||
|
*
|
||||||
|
* Used by: Resolvers
|
||||||
|
* See: StylingRestEndpoint.php
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/styling';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST path to persist data of this module to the WP DB.
|
||||||
|
*
|
||||||
|
* Used by: Controls
|
||||||
|
* See: StylingRestEndpoint.php
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/styling';
|
23
modules/ppcp-settings/resources/js/data/styling/controls.js
vendored
Normal file
23
modules/ppcp-settings/resources/js/data/styling/controls.js
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
export const controls = {
|
||||||
|
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
|
||||||
|
return await apiFetch( {
|
||||||
|
path: REST_PERSIST_PATH,
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
};
|
211
modules/ppcp-settings/resources/js/data/styling/hooks.js
Normal file
211
modules/ppcp-settings/resources/js/data/styling/hooks.js
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
/**
|
||||||
|
* Hooks: Provide the main API for components to interact with the store.
|
||||||
|
*
|
||||||
|
* These encapsulate store interactions, offering a consistent interface.
|
||||||
|
* Hooks simplify data access and manipulation for components.
|
||||||
|
*
|
||||||
|
* @file
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback } from '@wordpress/element';
|
||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
|
||||||
|
import { createHooksForStore } from '../utils';
|
||||||
|
import { STORE_NAME } from './constants';
|
||||||
|
import {
|
||||||
|
STYLING_COLORS,
|
||||||
|
STYLING_LABELS,
|
||||||
|
STYLING_LAYOUTS,
|
||||||
|
STYLING_LOCATIONS,
|
||||||
|
STYLING_PAYMENT_METHODS,
|
||||||
|
STYLING_SHAPES,
|
||||||
|
} from './configuration';
|
||||||
|
|
||||||
|
const useHooks = () => {
|
||||||
|
const { useTransient } = createHooksForStore( STORE_NAME );
|
||||||
|
const { persist, setPersistent } = useDispatch( STORE_NAME );
|
||||||
|
|
||||||
|
// Transient accessors.
|
||||||
|
const [ isReady ] = useTransient( 'isReady' );
|
||||||
|
const [ location, setLocation ] = useTransient( 'location' );
|
||||||
|
|
||||||
|
// Persistent accessors.
|
||||||
|
const persistentData = useSelect(
|
||||||
|
( select ) => select( STORE_NAME ).persistentData(),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getLocationProp = useCallback(
|
||||||
|
( locationId, prop ) => {
|
||||||
|
if ( undefined === persistentData[ locationId ]?.[ prop ] ) {
|
||||||
|
console.error(
|
||||||
|
`Trying to access non-existent style property: ${ locationId }.${ prop }. Possibly wrong style name - review the reducer.`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return persistentData[ locationId ][ prop ];
|
||||||
|
},
|
||||||
|
[ persistentData ]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setLocationProp = useCallback(
|
||||||
|
( locationId, prop, value ) => {
|
||||||
|
const updatedStyles = {
|
||||||
|
...persistentData[ locationId ],
|
||||||
|
[ prop ]: value,
|
||||||
|
};
|
||||||
|
|
||||||
|
setPersistent( locationId, updatedStyles );
|
||||||
|
},
|
||||||
|
[ persistentData, setPersistent ]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
persist,
|
||||||
|
isReady,
|
||||||
|
location,
|
||||||
|
setLocation,
|
||||||
|
getLocationProp,
|
||||||
|
setLocationProp,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useStore = () => {
|
||||||
|
const { persist, isReady } = useHooks();
|
||||||
|
return { persist, isReady };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useStylingLocation = () => {
|
||||||
|
const { location, setLocation } = useHooks();
|
||||||
|
return { location, setLocation };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLocationProps = ( location ) => {
|
||||||
|
const { getLocationProp, setLocationProp } = useHooks();
|
||||||
|
const details = STYLING_LOCATIONS[ location ] ?? {};
|
||||||
|
|
||||||
|
const sanitize = ( value ) => ( undefined === value ? true : !! value );
|
||||||
|
|
||||||
|
return {
|
||||||
|
choices: Object.values( STYLING_LOCATIONS ),
|
||||||
|
details,
|
||||||
|
isActive: sanitize( getLocationProp( location, 'enabled' ) ),
|
||||||
|
setActive: ( state ) =>
|
||||||
|
setLocationProp( location, 'enabled', sanitize( state ) ),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePaymentMethodProps = ( location ) => {
|
||||||
|
const { getLocationProp, setLocationProp } = useHooks();
|
||||||
|
|
||||||
|
const sanitize = ( value ) => {
|
||||||
|
if ( Array.isArray( value ) ) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value ? [ value ] : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
choices: Object.values( STYLING_PAYMENT_METHODS ),
|
||||||
|
paymentMethods: sanitize( getLocationProp( location, 'methods' ) ),
|
||||||
|
setPaymentMethods: ( methods ) =>
|
||||||
|
setLocationProp( location, 'methods', sanitize( methods ) ),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useColorProps = ( location ) => {
|
||||||
|
const { getLocationProp, setLocationProp } = useHooks();
|
||||||
|
|
||||||
|
const sanitize = ( value ) => {
|
||||||
|
const isValidColor = Object.values( STYLING_COLORS ).some(
|
||||||
|
( color ) => color.value === value
|
||||||
|
);
|
||||||
|
return isValidColor ? value : STYLING_COLORS.gold.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
choices: Object.values( STYLING_COLORS ),
|
||||||
|
color: sanitize( getLocationProp( location, 'color' ) ),
|
||||||
|
setColor: ( color ) =>
|
||||||
|
setLocationProp( location, 'color', sanitize( color ) ),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useShapeProps = ( location ) => {
|
||||||
|
const { getLocationProp, setLocationProp } = useHooks();
|
||||||
|
|
||||||
|
const sanitize = ( value ) => {
|
||||||
|
const isValidColor = Object.values( STYLING_SHAPES ).some(
|
||||||
|
( color ) => color.value === value
|
||||||
|
);
|
||||||
|
return isValidColor ? value : STYLING_SHAPES.rect.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
choices: Object.values( STYLING_SHAPES ),
|
||||||
|
shape: sanitize( getLocationProp( location, 'shape' ) ),
|
||||||
|
setShape: ( shape ) =>
|
||||||
|
setLocationProp( location, 'shape', sanitize( shape ) ),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLabelProps = ( location ) => {
|
||||||
|
const { getLocationProp, setLocationProp } = useHooks();
|
||||||
|
|
||||||
|
const sanitize = ( value ) => {
|
||||||
|
const isValidColor = Object.values( STYLING_LABELS ).some(
|
||||||
|
( color ) => color.value === value
|
||||||
|
);
|
||||||
|
return isValidColor ? value : STYLING_LABELS.paypal.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
choices: Object.values( STYLING_LABELS ),
|
||||||
|
label: sanitize( getLocationProp( location, 'label' ) ),
|
||||||
|
setLabel: ( label ) =>
|
||||||
|
setLocationProp( location, 'label', sanitize( label ) ),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLayoutProps = ( location ) => {
|
||||||
|
const { getLocationProp, setLocationProp } = useHooks();
|
||||||
|
const { details } = useLocationProps( location );
|
||||||
|
const isAvailable = false !== details.props.layout;
|
||||||
|
|
||||||
|
const sanitize = ( value ) => {
|
||||||
|
const isValidColor = Object.values( STYLING_LAYOUTS ).some(
|
||||||
|
( color ) => color.value === value
|
||||||
|
);
|
||||||
|
return isValidColor ? value : STYLING_LAYOUTS.vertical.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
choices: Object.values( STYLING_LAYOUTS ),
|
||||||
|
isAvailable,
|
||||||
|
layout: sanitize( getLocationProp( location, 'layout' ) ),
|
||||||
|
setLayout: ( layout ) =>
|
||||||
|
setLocationProp( location, 'layout', sanitize( layout ) ),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTaglineProps = ( location ) => {
|
||||||
|
const { getLocationProp, setLocationProp } = useHooks();
|
||||||
|
const { details } = useLocationProps( location );
|
||||||
|
|
||||||
|
// Tagline is only available for horizontal layouts.
|
||||||
|
const isAvailable =
|
||||||
|
false !== details.props.tagline &&
|
||||||
|
STYLING_LAYOUTS.horizontal.value ===
|
||||||
|
getLocationProp( location, 'layout' );
|
||||||
|
|
||||||
|
const sanitize = ( value ) => !! value;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAvailable,
|
||||||
|
tagline: isAvailable
|
||||||
|
? sanitize( getLocationProp( location, 'tagline' ) )
|
||||||
|
: false,
|
||||||
|
setTagline: ( tagline ) =>
|
||||||
|
setLocationProp( location, 'tagline', sanitize( tagline ) ),
|
||||||
|
};
|
||||||
|
};
|
24
modules/ppcp-settings/resources/js/data/styling/index.js
Normal file
24
modules/ppcp-settings/resources/js/data/styling/index.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { createReduxStore, register } from '@wordpress/data';
|
||||||
|
import { controls as wpControls } from '@wordpress/data-controls';
|
||||||
|
|
||||||
|
import { STORE_NAME } from './constants';
|
||||||
|
import reducer from './reducer';
|
||||||
|
import * as selectors from './selectors';
|
||||||
|
import * as actions from './actions';
|
||||||
|
import * as hooks from './hooks';
|
||||||
|
import { resolvers } from './resolvers';
|
||||||
|
import { controls } from './controls';
|
||||||
|
|
||||||
|
export const initStore = () => {
|
||||||
|
const store = createReduxStore( STORE_NAME, {
|
||||||
|
reducer,
|
||||||
|
controls: { ...wpControls, ...controls },
|
||||||
|
actions,
|
||||||
|
selectors,
|
||||||
|
resolvers,
|
||||||
|
} );
|
||||||
|
|
||||||
|
register( store );
|
||||||
|
};
|
||||||
|
|
||||||
|
export { hooks, selectors, STORE_NAME };
|
128
modules/ppcp-settings/resources/js/data/styling/reducer.js
Normal file
128
modules/ppcp-settings/resources/js/data/styling/reducer.js
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
/**
|
||||||
|
* Reducer: Defines store structure and state updates for this module.
|
||||||
|
*
|
||||||
|
* Manages both transient (temporary) and persistent (saved) state.
|
||||||
|
* The initial state must define all properties, as dynamic additions are not supported.
|
||||||
|
*
|
||||||
|
* @file
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createReducer, createSetters } from '../utils';
|
||||||
|
import ACTION_TYPES from './action-types';
|
||||||
|
import {
|
||||||
|
STYLING_COLORS,
|
||||||
|
STYLING_LABELS,
|
||||||
|
STYLING_LAYOUTS,
|
||||||
|
STYLING_LOCATIONS,
|
||||||
|
STYLING_SHAPES,
|
||||||
|
} from './configuration';
|
||||||
|
|
||||||
|
// Store structure.
|
||||||
|
|
||||||
|
// Transient: Values that are _not_ saved to the DB (like app lifecycle-flags).
|
||||||
|
const defaultTransient = Object.freeze( {
|
||||||
|
isReady: false,
|
||||||
|
location: STYLING_LOCATIONS.cart.value, // Which location is selected in the Styling tab.
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Persistent: Values that are loaded from the DB.
|
||||||
|
const defaultPersistent = Object.freeze( {
|
||||||
|
[ STYLING_LOCATIONS.cart.value ]: Object.freeze( {
|
||||||
|
enabled: true,
|
||||||
|
methods: [],
|
||||||
|
label: STYLING_LABELS.pay.value,
|
||||||
|
shape: STYLING_SHAPES.rect.value,
|
||||||
|
color: STYLING_COLORS.gold.value,
|
||||||
|
} ),
|
||||||
|
[ STYLING_LOCATIONS.classicCheckout.value ]: Object.freeze( {
|
||||||
|
enabled: true,
|
||||||
|
methods: [],
|
||||||
|
label: STYLING_LABELS.checkout.value,
|
||||||
|
shape: STYLING_SHAPES.rect.value,
|
||||||
|
color: STYLING_COLORS.gold.value,
|
||||||
|
layout: STYLING_LAYOUTS.vertical.value,
|
||||||
|
tagline: false,
|
||||||
|
} ),
|
||||||
|
[ STYLING_LOCATIONS.expressCheckout.value ]: Object.freeze( {
|
||||||
|
enabled: true,
|
||||||
|
methods: [],
|
||||||
|
label: STYLING_LABELS.checkout.value,
|
||||||
|
shape: STYLING_SHAPES.rect.value,
|
||||||
|
color: STYLING_COLORS.gold.value,
|
||||||
|
} ),
|
||||||
|
[ STYLING_LOCATIONS.miniCart.value ]: Object.freeze( {
|
||||||
|
enabled: true,
|
||||||
|
methods: [],
|
||||||
|
label: STYLING_LABELS.pay.value,
|
||||||
|
shape: STYLING_SHAPES.rect.value,
|
||||||
|
color: STYLING_COLORS.gold.value,
|
||||||
|
layout: STYLING_LAYOUTS.vertical.value,
|
||||||
|
tagline: false,
|
||||||
|
} ),
|
||||||
|
[ STYLING_LOCATIONS.product.value ]: Object.freeze( {
|
||||||
|
enabled: true,
|
||||||
|
methods: [],
|
||||||
|
label: STYLING_LABELS.buynow.value,
|
||||||
|
shape: STYLING_SHAPES.rect.value,
|
||||||
|
color: STYLING_COLORS.gold.value,
|
||||||
|
layout: STYLING_LAYOUTS.vertical.value,
|
||||||
|
tagline: false,
|
||||||
|
} ),
|
||||||
|
} );
|
||||||
|
|
||||||
|
const sanitizeLocation = ( oldDetails, newDetails ) => {
|
||||||
|
// Skip if provided details are not a plain object.
|
||||||
|
if (
|
||||||
|
! newDetails ||
|
||||||
|
'object' !== typeof newDetails ||
|
||||||
|
Array.isArray( newDetails )
|
||||||
|
) {
|
||||||
|
return oldDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...oldDetails, ...newDetails };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reducer logic.
|
||||||
|
|
||||||
|
const [ setTransient, setPersistent ] = createSetters(
|
||||||
|
defaultTransient,
|
||||||
|
defaultPersistent
|
||||||
|
);
|
||||||
|
|
||||||
|
const reducer = createReducer( defaultTransient, defaultPersistent, {
|
||||||
|
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
|
||||||
|
setTransient( state, payload ),
|
||||||
|
|
||||||
|
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) =>
|
||||||
|
setPersistent( state, payload ),
|
||||||
|
|
||||||
|
[ ACTION_TYPES.RESET ]: ( state ) => {
|
||||||
|
const cleanState = setTransient(
|
||||||
|
setPersistent( state, defaultPersistent ),
|
||||||
|
defaultTransient
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep "read-only" details and initialization flags.
|
||||||
|
cleanState.isReady = true;
|
||||||
|
|
||||||
|
return cleanState;
|
||||||
|
},
|
||||||
|
|
||||||
|
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
|
||||||
|
const validData = Object.keys( defaultPersistent ).reduce(
|
||||||
|
( data, location ) => {
|
||||||
|
data[ location ] = sanitizeLocation(
|
||||||
|
state.data[ location ],
|
||||||
|
payload.data[ location ]
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
return setPersistent( state, validData );
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
export default reducer;
|
36
modules/ppcp-settings/resources/js/data/styling/resolvers.js
Normal file
36
modules/ppcp-settings/resources/js/data/styling/resolvers.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* Resolvers: Handle asynchronous data fetching for the store.
|
||||||
|
*
|
||||||
|
* These functions update store state with data from external sources.
|
||||||
|
* Each resolver corresponds to a specific selector (selector with same name must exist).
|
||||||
|
* Resolvers are called automatically when selectors request unavailable data.
|
||||||
|
*
|
||||||
|
* @file
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { dispatch } from '@wordpress/data';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { apiFetch } from '@wordpress/data-controls';
|
||||||
|
|
||||||
|
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
|
||||||
|
|
||||||
|
export const resolvers = {
|
||||||
|
/**
|
||||||
|
* Retrieve settings from the site's REST API.
|
||||||
|
*/
|
||||||
|
*persistentData() {
|
||||||
|
try {
|
||||||
|
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
|
||||||
|
|
||||||
|
yield dispatch( STORE_NAME ).hydrate( result );
|
||||||
|
yield dispatch( STORE_NAME ).setIsReady( true );
|
||||||
|
} catch ( e ) {
|
||||||
|
yield dispatch( 'core/notices' ).createErrorNotice(
|
||||||
|
__(
|
||||||
|
'Error retrieving style-details.',
|
||||||
|
'woocommerce-paypal-payments'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
21
modules/ppcp-settings/resources/js/data/styling/selectors.js
Normal file
21
modules/ppcp-settings/resources/js/data/styling/selectors.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Selectors: Extract specific pieces of state from the store.
|
||||||
|
*
|
||||||
|
* These functions provide a consistent interface for accessing store data.
|
||||||
|
* They allow components to retrieve data without knowing the store structure.
|
||||||
|
*
|
||||||
|
* @file
|
||||||
|
*/
|
||||||
|
|
||||||
|
const EMPTY_OBJ = Object.freeze( {} );
|
||||||
|
|
||||||
|
const getState = ( state ) => state || EMPTY_OBJ;
|
||||||
|
|
||||||
|
export const persistentData = ( state ) => {
|
||||||
|
return getState( state ).data || EMPTY_OBJ;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transientData = ( state ) => {
|
||||||
|
const { data, ...transientState } = getState( state );
|
||||||
|
return transientState || EMPTY_OBJ;
|
||||||
|
};
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
|
import { useCallback } from '@wordpress/element';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an object with new values, filtering based on allowed keys.
|
* Updates an object with new values, filtering based on allowed keys.
|
||||||
*
|
*
|
||||||
|
@ -13,6 +16,10 @@ const updateObject = ( oldObject, newValues, allowedKeys = {} ) => ( {
|
||||||
...Object.keys( newValues ).reduce( ( acc, key ) => {
|
...Object.keys( newValues ).reduce( ( acc, key ) => {
|
||||||
if ( key in allowedKeys ) {
|
if ( key in allowedKeys ) {
|
||||||
acc[ key ] = newValues[ key ];
|
acc[ key ] = newValues[ key ];
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`Ignoring unknown key "${ key }" - to use it, add it to the initial store properties in the reducer.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} ),
|
}, {} ),
|
||||||
|
@ -73,3 +80,63 @@ export const createReducer = (
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object with two hooks:
|
||||||
|
* - useTransient( prop )
|
||||||
|
* - usePersistent( prop )
|
||||||
|
*
|
||||||
|
* Both hooks have a similar syntax to the native "useState( prop )" hook, but provide access to
|
||||||
|
* a transient or persistent property in the relevant Redux store.
|
||||||
|
*
|
||||||
|
* Sample:
|
||||||
|
*
|
||||||
|
* const { useTransient } = createHooksForStore( STORE_NAME );
|
||||||
|
* const [ isReady, setIsReady ] = useTransient( 'isReady' );
|
||||||
|
*
|
||||||
|
* @param {string} storeName Store name.
|
||||||
|
* @return {{useTransient, usePersistent}} Store hooks.
|
||||||
|
*/
|
||||||
|
export const createHooksForStore = ( storeName ) => {
|
||||||
|
const createHook = ( selector, dispatcher ) => ( key ) => {
|
||||||
|
const value = useSelect(
|
||||||
|
( select ) => {
|
||||||
|
const store = select( storeName );
|
||||||
|
if ( ! store?.[ selector ] ) {
|
||||||
|
throw new Error(
|
||||||
|
`Please create the selector "${ selector }" for store "${ storeName }"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const selectorResult = store[ selector ]();
|
||||||
|
if ( undefined === selectorResult?.[ key ] ) {
|
||||||
|
console.error(
|
||||||
|
`Warning: ${ selector }()[${ key }] is undefined in store "${ storeName }". This may indicate a bug.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return selectorResult?.[ key ];
|
||||||
|
},
|
||||||
|
[ key ]
|
||||||
|
);
|
||||||
|
|
||||||
|
const actions = useDispatch( storeName );
|
||||||
|
|
||||||
|
const setValue = useCallback(
|
||||||
|
( newValue ) => {
|
||||||
|
if ( ! actions?.[ dispatcher ] ) {
|
||||||
|
throw new Error(
|
||||||
|
`Please create the action "${ dispatcher }" for store "${ storeName }"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
actions[ dispatcher ]( key, newValue );
|
||||||
|
},
|
||||||
|
[ actions, key ]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [ value, setValue ];
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
useTransient: createHook( 'transientData', 'setTransient' ),
|
||||||
|
usePersistent: createHook( 'persistentData', 'setPersistent' ),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -26,6 +26,9 @@ use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager;
|
||||||
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
|
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
|
||||||
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
|
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
|
||||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||||
|
use WooCommerce\PayPalCommerce\Settings\Endpoint\StylingRestEndpoint;
|
||||||
|
use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings;
|
||||||
|
use WooCommerce\PayPalCommerce\Settings\Service\DataSanitizer;
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'settings.url' => static function ( ContainerInterface $container ) : string {
|
'settings.url' => static function ( ContainerInterface $container ) : string {
|
||||||
|
@ -66,12 +69,23 @@ return array(
|
||||||
$container->get( 'wcgateway.is-send-only-country' )
|
$container->get( 'wcgateway.is-send-only-country' )
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
'settings.data.styling' => static function ( ContainerInterface $container ) : StylingSettings {
|
||||||
|
return new StylingSettings(
|
||||||
|
$container->get( 'settings.service.sanitizer' )
|
||||||
|
);
|
||||||
|
},
|
||||||
'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint {
|
'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint {
|
||||||
return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) );
|
return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) );
|
||||||
},
|
},
|
||||||
'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint {
|
'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint {
|
||||||
return new CommonRestEndpoint( $container->get( 'settings.data.general' ) );
|
return new CommonRestEndpoint( $container->get( 'settings.data.general' ) );
|
||||||
},
|
},
|
||||||
|
'settings.rest.styling' => static function ( ContainerInterface $container ) : StylingRestEndpoint {
|
||||||
|
return new StylingRestEndpoint(
|
||||||
|
$container->get( 'settings.data.styling' ),
|
||||||
|
$container->get( 'settings.service.sanitizer' )
|
||||||
|
);
|
||||||
|
},
|
||||||
'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint {
|
'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint {
|
||||||
return new RefreshFeatureStatusEndpoint(
|
return new RefreshFeatureStatusEndpoint(
|
||||||
$container->get( 'wcgateway.settings' ),
|
$container->get( 'wcgateway.settings' ),
|
||||||
|
@ -189,6 +203,9 @@ return array(
|
||||||
$container->get( 'woocommerce.logger.woocommerce' ),
|
$container->get( 'woocommerce.logger.woocommerce' ),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
'settings.service.sanitizer' => static function ( ContainerInterface $container ) : DataSanitizer {
|
||||||
|
return new DataSanitizer();
|
||||||
|
},
|
||||||
'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint {
|
'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint {
|
||||||
return new SwitchSettingsUiEndpoint(
|
return new SwitchSettingsUiEndpoint(
|
||||||
$container->get( 'woocommerce.logger.woocommerce' ),
|
$container->get( 'woocommerce.logger.woocommerce' ),
|
||||||
|
|
105
modules/ppcp-settings/src/DTO/LocationStylingDTO.php
Normal file
105
modules/ppcp-settings/src/DTO/LocationStylingDTO.php
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Data transfer object. Stores styling details for a single location.
|
||||||
|
*
|
||||||
|
* @package WooCommerce\PayPalCommerce\Settings\DTO;
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types = 1 );
|
||||||
|
|
||||||
|
namespace WooCommerce\PayPalCommerce\Settings\DTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO that collects all styling details of a single location
|
||||||
|
*
|
||||||
|
* Intentionally has no internal logic, sanitation or validation.
|
||||||
|
*/
|
||||||
|
class LocationStylingDTO {
|
||||||
|
/**
|
||||||
|
* The location name.
|
||||||
|
*
|
||||||
|
* @var string [cart|classic_checkout|express_checkout|mini_cart|product]
|
||||||
|
*/
|
||||||
|
public string $location;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether PayPal payments are enabled on this location.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public bool $enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of active payment methods, e.g., 'venmo', 'applepay', ...
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public array $methods;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shape of buttons on this location.
|
||||||
|
*
|
||||||
|
* @var string [rect|pill]
|
||||||
|
*/
|
||||||
|
public string $shape;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label of the button on this location.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color of the button on this location.
|
||||||
|
*
|
||||||
|
* @var string [gold|blue|silver|black|white]
|
||||||
|
*/
|
||||||
|
public string $color;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The button layout
|
||||||
|
*
|
||||||
|
* @var string [horizontal|vertical]
|
||||||
|
*/
|
||||||
|
public string $layout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to show a tagline below the buttons.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public bool $tagline;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param string $location The location name.
|
||||||
|
* @param bool $enabled Whether PayPal payments are enabled on this location.
|
||||||
|
* @param array $methods List of active payment methods.
|
||||||
|
* @param string $shape Shape of buttons on this location.
|
||||||
|
* @param string $label Label of the button on this location.
|
||||||
|
* @param string $color Color of the button on this location.
|
||||||
|
* @param string $layout Horizontal or vertical button layout.
|
||||||
|
* @param bool $tagline Whether to show a tagline below the buttons.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
string $location = '',
|
||||||
|
bool $enabled = true,
|
||||||
|
array $methods = array(),
|
||||||
|
string $shape = 'rect',
|
||||||
|
string $label = 'pay',
|
||||||
|
string $color = 'gold',
|
||||||
|
string $layout = 'vertical',
|
||||||
|
bool $tagline = false
|
||||||
|
) {
|
||||||
|
$this->location = $location;
|
||||||
|
$this->enabled = $enabled;
|
||||||
|
$this->methods = $methods;
|
||||||
|
$this->shape = $shape;
|
||||||
|
$this->label = $label;
|
||||||
|
$this->color = $color;
|
||||||
|
$this->layout = $layout;
|
||||||
|
$this->tagline = $tagline;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* StylingSettings class
|
* Styling details class
|
||||||
*
|
*
|
||||||
* @package WooCommerce\PayPalCommerce\Settings\Data
|
* @package WooCommerce\PayPalCommerce\Settings\Data
|
||||||
*/
|
*/
|
||||||
|
@ -10,9 +10,13 @@ declare( strict_types = 1 );
|
||||||
namespace WooCommerce\PayPalCommerce\Settings\Data;
|
namespace WooCommerce\PayPalCommerce\Settings\Data;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO;
|
||||||
|
use WooCommerce\PayPalCommerce\Settings\Service\DataSanitizer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class serves as a container for managing the styling settings.
|
* Class StylingSettings
|
||||||
|
*
|
||||||
|
* Stores and manages the styling details.
|
||||||
*/
|
*/
|
||||||
class StylingSettings extends AbstractDataModel {
|
class StylingSettings extends AbstractDataModel {
|
||||||
|
|
||||||
|
@ -21,7 +25,26 @@ class StylingSettings extends AbstractDataModel {
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected const OPTION_KEY = 'woocommerce-ppcp-data-styling-settings';
|
protected const OPTION_KEY = 'woocommerce-ppcp-data-styling';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data sanitizer service.
|
||||||
|
*
|
||||||
|
* @var DataSanitizer
|
||||||
|
*/
|
||||||
|
protected DataSanitizer $sanitizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param DataSanitizer $sanitizer Data sanitizer service.
|
||||||
|
* @throws RuntimeException If the OPTION_KEY is not defined in the child class.
|
||||||
|
*/
|
||||||
|
public function __construct( DataSanitizer $sanitizer ) {
|
||||||
|
$this->sanitizer = $sanitizer;
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get default values for the model.
|
* Get default values for the model.
|
||||||
|
@ -29,6 +52,107 @@ class StylingSettings extends AbstractDataModel {
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function get_defaults() : array {
|
protected function get_defaults() : array {
|
||||||
return array();
|
return array(
|
||||||
|
'cart' => new LocationStylingDTO( 'cart' ),
|
||||||
|
'classic_checkout' => new LocationStylingDTO( 'classic_checkout' ),
|
||||||
|
'express_checkout' => new LocationStylingDTO( 'express_checkout' ),
|
||||||
|
'mini_cart' => new LocationStylingDTO( 'mini_cart' ),
|
||||||
|
'product' => new LocationStylingDTO( 'product' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get styling details for Cart and Block Cart.
|
||||||
|
*
|
||||||
|
* @return LocationStylingDTO
|
||||||
|
*/
|
||||||
|
public function get_cart() : LocationStylingDTO {
|
||||||
|
return $this->data['cart'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set styling details for Cart and Block Cart.
|
||||||
|
*
|
||||||
|
* @param mixed $styles The new styling details.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set_cart( $styles ) : void {
|
||||||
|
$this->data['cart'] = $this->sanitizer->sanitize_location_style( $styles );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get styling details for Classic Checkout.
|
||||||
|
*
|
||||||
|
* @return LocationStylingDTO
|
||||||
|
*/
|
||||||
|
public function get_classic_checkout() : LocationStylingDTO {
|
||||||
|
return $this->data['classic_checkout'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set styling details for Classic Checkout.
|
||||||
|
*
|
||||||
|
* @param mixed $styles The new styling details.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set_classic_checkout( $styles ) : void {
|
||||||
|
$this->data['classic_checkout'] = $this->sanitizer->sanitize_location_style( $styles );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get styling details for Express Checkout.
|
||||||
|
*
|
||||||
|
* @return LocationStylingDTO
|
||||||
|
*/
|
||||||
|
public function get_express_checkout() : LocationStylingDTO {
|
||||||
|
return $this->data['express_checkout'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set styling details for Express Checkout.
|
||||||
|
*
|
||||||
|
* @param mixed $styles The new styling details.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set_express_checkout( $styles ) : void {
|
||||||
|
$this->data['express_checkout'] = $this->sanitizer->sanitize_location_style( $styles );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get styling details for Mini Cart
|
||||||
|
*
|
||||||
|
* @return LocationStylingDTO
|
||||||
|
*/
|
||||||
|
public function get_mini_cart() : LocationStylingDTO {
|
||||||
|
return $this->data['mini_cart'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set styling details for Mini Cart.
|
||||||
|
*
|
||||||
|
* @param mixed $styles The new styling details.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set_mini_cart( $styles ) : void {
|
||||||
|
$this->data['mini_cart'] = $this->sanitizer->sanitize_location_style( $styles );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get styling details for Product Page.
|
||||||
|
*
|
||||||
|
* @return LocationStylingDTO
|
||||||
|
*/
|
||||||
|
public function get_product() : LocationStylingDTO {
|
||||||
|
return $this->data['product'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set styling details for Product Page.
|
||||||
|
*
|
||||||
|
* @param mixed $styles The new styling details.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set_product( $styles ) : void {
|
||||||
|
$this->data['product'] = $this->sanitizer->sanitize_location_style( $styles );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ class OnboardingRestEndpoint extends RestEndpoint {
|
||||||
public function __construct( OnboardingProfile $profile ) {
|
public function __construct( OnboardingProfile $profile ) {
|
||||||
$this->profile = $profile;
|
$this->profile = $profile;
|
||||||
|
|
||||||
$this->field_map['products']['sanitize'] = fn( $list ) => array_map( 'sanitize_text_field', $list );
|
$this->field_map['products']['sanitize'] = static fn( $list ) => array_map( 'sanitize_text_field', $list );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -112,9 +112,9 @@ abstract class RestEndpoint extends WC_REST_Controller {
|
||||||
if ( null === $sanitation_cb ) {
|
if ( null === $sanitation_cb ) {
|
||||||
$sanitized[ $key ] = $value;
|
$sanitized[ $key ] = $value;
|
||||||
} elseif ( is_string( $sanitation_cb ) && method_exists( $this, $sanitation_cb ) ) {
|
} elseif ( is_string( $sanitation_cb ) && method_exists( $this, $sanitation_cb ) ) {
|
||||||
$sanitized[ $key ] = $this->{$sanitation_cb}( $value );
|
$sanitized[ $key ] = $this->{$sanitation_cb}( $value, $key );
|
||||||
} elseif ( is_callable( $sanitation_cb ) ) {
|
} elseif ( is_callable( $sanitation_cb ) ) {
|
||||||
$sanitized[ $key ] = $sanitation_cb( $value );
|
$sanitized[ $key ] = $sanitation_cb( $value, $key );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
170
modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php
Normal file
170
modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST endpoint to manage the styling page.
|
||||||
|
*
|
||||||
|
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types = 1 );
|
||||||
|
|
||||||
|
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
|
||||||
|
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings;
|
||||||
|
use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO;
|
||||||
|
use WooCommerce\PayPalCommerce\Settings\Service\DataSanitizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST controller for the "Styling" settings tab.
|
||||||
|
*
|
||||||
|
* This API acts as the intermediary between the "external world" and our
|
||||||
|
* internal data model.
|
||||||
|
*/
|
||||||
|
class StylingRestEndpoint extends RestEndpoint {
|
||||||
|
/**
|
||||||
|
* The base path for this REST controller.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'styling';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The settings instance.
|
||||||
|
*
|
||||||
|
* @var StylingSettings
|
||||||
|
*/
|
||||||
|
protected StylingSettings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data sanitizer service.
|
||||||
|
*
|
||||||
|
* @var DataSanitizer
|
||||||
|
*/
|
||||||
|
protected DataSanitizer $sanitizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field mapping for request to profile transformation.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $field_map = array(
|
||||||
|
'cart' => array(
|
||||||
|
'js_name' => 'cart',
|
||||||
|
),
|
||||||
|
'classic_checkout' => array(
|
||||||
|
'js_name' => 'classicCheckout',
|
||||||
|
),
|
||||||
|
'express_checkout' => array(
|
||||||
|
'js_name' => 'expressCheckout',
|
||||||
|
),
|
||||||
|
'mini_cart' => array(
|
||||||
|
'js_name' => 'miniCart',
|
||||||
|
),
|
||||||
|
'product' => array(
|
||||||
|
'js_name' => 'product',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param StylingSettings $settings The settings instance.
|
||||||
|
* @param DataSanitizer $sanitizer Data sanitizer service.
|
||||||
|
*/
|
||||||
|
public function __construct( StylingSettings $settings, DataSanitizer $sanitizer ) {
|
||||||
|
$this->settings = $settings;
|
||||||
|
$this->sanitizer = $sanitizer;
|
||||||
|
|
||||||
|
$this->field_map['cart']['sanitize'] = array(
|
||||||
|
$this->sanitizer,
|
||||||
|
'sanitize_location_style',
|
||||||
|
);
|
||||||
|
$this->field_map['classic_checkout']['sanitize'] = array(
|
||||||
|
$this->sanitizer,
|
||||||
|
'sanitize_location_style',
|
||||||
|
);
|
||||||
|
$this->field_map['express_checkout']['sanitize'] = array(
|
||||||
|
$this->sanitizer,
|
||||||
|
'sanitize_location_style',
|
||||||
|
);
|
||||||
|
$this->field_map['mini_cart']['sanitize'] = array(
|
||||||
|
$this->sanitizer,
|
||||||
|
'sanitize_location_style',
|
||||||
|
);
|
||||||
|
$this->field_map['product']['sanitize'] = array(
|
||||||
|
$this->sanitizer,
|
||||||
|
'sanitize_location_style',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure REST API routes.
|
||||||
|
*/
|
||||||
|
public function register_routes() : void {
|
||||||
|
/**
|
||||||
|
* GET wc/v3/wc_paypal/styling
|
||||||
|
*/
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base,
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_details' ),
|
||||||
|
'permission_callback' => array( $this, 'check_permission' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST wc/v3/wc_paypal/styling
|
||||||
|
* {
|
||||||
|
* // Fields mentioned in $field_map[]['js_name']
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base,
|
||||||
|
array(
|
||||||
|
'methods' => WP_REST_Server::EDITABLE,
|
||||||
|
'callback' => array( $this, 'update_details' ),
|
||||||
|
'permission_callback' => array( $this, 'check_permission' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all styling details.
|
||||||
|
*
|
||||||
|
* @return WP_REST_Response The current styling details.
|
||||||
|
*/
|
||||||
|
public function get_details() : WP_REST_Response {
|
||||||
|
$js_data = $this->sanitize_for_javascript(
|
||||||
|
$this->settings->to_array(),
|
||||||
|
$this->field_map
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->return_success(
|
||||||
|
$js_data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates styling details based on the request.
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Full data about the request.
|
||||||
|
*
|
||||||
|
* @return WP_REST_Response The updated styling details.
|
||||||
|
*/
|
||||||
|
public function update_details( WP_REST_Request $request ) : WP_REST_Response {
|
||||||
|
$wp_data = $this->sanitize_for_wordpress(
|
||||||
|
$request->get_params(),
|
||||||
|
$this->field_map
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->settings->from_array( $wp_data );
|
||||||
|
$this->settings->save();
|
||||||
|
|
||||||
|
return $this->get_details();
|
||||||
|
}
|
||||||
|
}
|
105
modules/ppcp-settings/src/Service/DataSanitizer.php
Normal file
105
modules/ppcp-settings/src/Service/DataSanitizer.php
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Provides data sanitization logic.
|
||||||
|
*
|
||||||
|
* @package WooCommerce\PayPalCommerce\Settings\Service
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare( strict_types = 1 );
|
||||||
|
|
||||||
|
namespace WooCommerce\PayPalCommerce\Settings\Service;
|
||||||
|
|
||||||
|
use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DataSanitizer service. Generally used by REST endpoints (sanitize input data)
|
||||||
|
* and data models (sanitize data during DB access)
|
||||||
|
*/
|
||||||
|
class DataSanitizer {
|
||||||
|
/**
|
||||||
|
* Sanitizes the provided styling data.
|
||||||
|
*
|
||||||
|
* @param mixed $data The styling data to sanitize.
|
||||||
|
* @param ?string $location Name of the location.
|
||||||
|
* @return LocationStylingDTO Styling data.
|
||||||
|
*/
|
||||||
|
public function sanitize_location_style( $data, string $location = null ) : LocationStylingDTO {
|
||||||
|
if ( $data instanceof LocationStylingDTO ) {
|
||||||
|
if ( $location ) {
|
||||||
|
$data->location = $location;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_object( $data ) ) {
|
||||||
|
$data = (array) $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! is_array( $data ) ) {
|
||||||
|
return new LocationStylingDTO( $location ?? '' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( null === $location ) {
|
||||||
|
$location = $data['location'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$is_enabled = $this->sanitize_bool( $data['enabled'] ?? true );
|
||||||
|
$shape = $this->sanitize_text( $data['shape'] ?? 'rect' );
|
||||||
|
$label = $this->sanitize_text( $data['label'] ?? 'pay' );
|
||||||
|
$color = $this->sanitize_text( $data['color'] ?? 'gold' );
|
||||||
|
$layout = $this->sanitize_text( $data['layout'] ?? 'vertical' );
|
||||||
|
$tagline = $this->sanitize_bool( $data['tagline'] ?? false );
|
||||||
|
$methods = $this->sanitize_array(
|
||||||
|
$data['methods'] ?? array(),
|
||||||
|
array( $this, 'sanitize_text' )
|
||||||
|
);
|
||||||
|
|
||||||
|
return new LocationStylingDTO(
|
||||||
|
$location,
|
||||||
|
$is_enabled,
|
||||||
|
$methods,
|
||||||
|
$shape,
|
||||||
|
$label,
|
||||||
|
$color,
|
||||||
|
$layout,
|
||||||
|
$tagline
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper. Ensures the value is a string.
|
||||||
|
*
|
||||||
|
* @param mixed $value Value to sanitize.
|
||||||
|
* @param string $default Default value.
|
||||||
|
* @return string Sanitized string.
|
||||||
|
*/
|
||||||
|
protected function sanitize_text( $value, string $default = '' ) : string {
|
||||||
|
return sanitize_text_field( $value ?? $default );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper. Ensures the value is a boolean.
|
||||||
|
*
|
||||||
|
* @param mixed $value Value to sanitize.
|
||||||
|
* @return bool Sanitized boolean.
|
||||||
|
*/
|
||||||
|
protected function sanitize_bool( $value ) : bool {
|
||||||
|
return filter_var( $value, FILTER_VALIDATE_BOOLEAN );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper. Ensures the value is an array and all items are sanitized.
|
||||||
|
*
|
||||||
|
* @param null|array $array Value to sanitize.
|
||||||
|
* @param callable $sanitize_callback Callback to sanitize each item in the array.
|
||||||
|
* @return array Array with sanitized items.
|
||||||
|
*/
|
||||||
|
protected function sanitize_array( ?array $array, callable $sanitize_callback ) : array {
|
||||||
|
if ( ! is_array( $array ) ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_map( $sanitize_callback, $array );
|
||||||
|
}
|
||||||
|
}
|
|
@ -212,6 +212,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
||||||
'webhooks' => $container->get( 'settings.rest.webhooks' ),
|
'webhooks' => $container->get( 'settings.rest.webhooks' ),
|
||||||
'refresh_feature_status' => $container->get( 'settings.rest.refresh_feature_status' ),
|
'refresh_feature_status' => $container->get( 'settings.rest.refresh_feature_status' ),
|
||||||
'settings' => $container->get( 'settings.rest.settings' ),
|
'settings' => $container->get( 'settings.rest.settings' ),
|
||||||
|
'styling' => $container->get( 'settings.rest.styling' ),
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ( $endpoints as $endpoint ) {
|
foreach ( $endpoints as $endpoint ) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue