diff --git a/modules/ppcp-button/resources/js/modules/Helper/CardFieldsHelper.js b/modules/ppcp-button/resources/js/modules/Helper/CardFieldsHelper.js new file mode 100644 index 000000000..5dc02d3d6 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/CardFieldsHelper.js @@ -0,0 +1,50 @@ +export const cardFieldStyles = (field) => { + const allowedProperties = [ + 'appearance', + 'color', + 'direction', + 'font', + 'font-family', + 'font-size', + 'font-size-adjust', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-variant-alternates', + 'font-variant-caps', + 'font-variant-east-asian', + 'font-variant-ligatures', + 'font-variant-numeric', + 'font-weight', + 'letter-spacing', + 'line-height', + 'opacity', + 'outline', + 'padding', + 'padding-bottom', + 'padding-left', + 'padding-right', + 'padding-top', + 'text-shadow', + 'transition', + '-moz-appearance', + '-moz-osx-font-smoothing', + '-moz-tap-highlight-color', + '-moz-transition', + '-webkit-appearance', + '-webkit-osx-font-smoothing', + '-webkit-tap-highlight-color', + '-webkit-transition', + ]; + + const stylesRaw = window.getComputedStyle(field); + const styles = {}; + Object.values(stylesRaw).forEach((prop) => { + if (!stylesRaw[prop] || !allowedProperties.includes(prop)) { + return; + } + styles[prop] = '' + stylesRaw[prop]; + }); + + return styles; +} diff --git a/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js b/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js index 6c7dc1a37..e3df5a27e 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js +++ b/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js @@ -87,3 +87,11 @@ export const loadPaypalJsScript = (options, buttons, container) => { paypal.Buttons(buttons).render(container); }); } + +export const loadPaypalJsScriptPromise = (options) => { + return new Promise((resolve, reject) => { + loadScript(options) + .then(resolve) + .catch(reject); + }); +} diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsRenderer.js index 3fc4e29f9..edad0ec6c 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CardFieldsRenderer.js @@ -1,4 +1,5 @@ import {show} from "../Helper/Hiding"; +import {cardFieldStyles} from "../Helper/CardFieldsHelper"; class CardFieldsRenderer { @@ -53,28 +54,28 @@ class CardFieldsRenderer { if (cardField.isEligible()) { const nameField = document.getElementById('ppcp-credit-card-gateway-card-name'); if (nameField) { - let styles = this.cardFieldStyles(nameField); + let styles = cardFieldStyles(nameField); cardField.NameField({style: {'input': styles}}).render(nameField.parentNode); nameField.remove(); } const numberField = document.getElementById('ppcp-credit-card-gateway-card-number'); if (numberField) { - let styles = this.cardFieldStyles(numberField); + let styles = cardFieldStyles(numberField); cardField.NumberField({style: {'input': styles}}).render(numberField.parentNode); numberField.remove(); } const expiryField = document.getElementById('ppcp-credit-card-gateway-card-expiry'); if (expiryField) { - let styles = this.cardFieldStyles(expiryField); + let styles = cardFieldStyles(expiryField); cardField.ExpiryField({style: {'input': styles}}).render(expiryField.parentNode); expiryField.remove(); } const cvvField = document.getElementById('ppcp-credit-card-gateway-card-cvc'); if (cvvField) { - let styles = this.cardFieldStyles(cvvField); + let styles = cardFieldStyles(cvvField); cardField.CVVField({style: {'input': styles}}).render(cvvField.parentNode); cvvField.remove(); } @@ -118,57 +119,6 @@ class CardFieldsRenderer { }); } - cardFieldStyles(field) { - const allowedProperties = [ - 'appearance', - 'color', - 'direction', - 'font', - 'font-family', - 'font-size', - 'font-size-adjust', - 'font-stretch', - 'font-style', - 'font-variant', - 'font-variant-alternates', - 'font-variant-caps', - 'font-variant-east-asian', - 'font-variant-ligatures', - 'font-variant-numeric', - 'font-weight', - 'letter-spacing', - 'line-height', - 'opacity', - 'outline', - 'padding', - 'padding-bottom', - 'padding-left', - 'padding-right', - 'padding-top', - 'text-shadow', - 'transition', - '-moz-appearance', - '-moz-osx-font-smoothing', - '-moz-tap-highlight-color', - '-moz-transition', - '-webkit-appearance', - '-webkit-osx-font-smoothing', - '-webkit-tap-highlight-color', - '-webkit-transition', - ]; - - const stylesRaw = window.getComputedStyle(field); - const styles = {}; - Object.values(stylesRaw).forEach((prop) => { - if (!stylesRaw[prop] || !allowedProperties.includes(prop)) { - return; - } - styles[prop] = '' + stylesRaw[prop]; - }); - - return styles; - } - disableFields() {} enableFields() {} } diff --git a/modules/ppcp-save-payment-methods/package.json b/modules/ppcp-save-payment-methods/package.json index a1e23ce17..9556097d6 100644 --- a/modules/ppcp-save-payment-methods/package.json +++ b/modules/ppcp-save-payment-methods/package.json @@ -10,7 +10,8 @@ "Edge >= 14" ], "dependencies": { - "core-js": "^3.25.0" + "core-js": "^3.25.0", + "@paypal/paypal-js": "^6.0.0" }, "devDependencies": { "@babel/core": "^7.19", diff --git a/modules/ppcp-save-payment-methods/resources/js/add-payment-method.js b/modules/ppcp-save-payment-methods/resources/js/add-payment-method.js index 200c2b87b..963fe0dc6 100644 --- a/modules/ppcp-save-payment-methods/resources/js/add-payment-method.js +++ b/modules/ppcp-save-payment-methods/resources/js/add-payment-method.js @@ -3,59 +3,17 @@ import { ORDER_BUTTON_SELECTOR, PaymentMethods } from "../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState"; - +import {loadScript} from "@paypal/paypal-js"; import { setVisible, setVisibleByClass } from "../../../ppcp-button/resources/js/modules/Helper/Hiding"; -import {loadPaypalJsScript} from "../../../ppcp-button/resources/js/modules/Helper/ScriptLoading"; - -loadPaypalJsScript( - { - clientId: ppcp_add_payment_method.client_id, - merchantId: ppcp_add_payment_method.merchant_id, - dataUserIdToken: ppcp_add_payment_method.id_token, - }, - { - createVaultSetupToken: async () => { - const response = await fetch(ppcp_add_payment_method.ajax.create_setup_token.endpoint, { - method: "POST", - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - nonce: ppcp_add_payment_method.ajax.create_setup_token.nonce, - }) - }) - - const result = await response.json() - - if(result.data.id) { - return result.data.id - } - }, - onApprove: async ({ vaultSetupToken }) => { - const response = await fetch(ppcp_add_payment_method.ajax.create_payment_token.endpoint, { - method: "POST", - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - nonce: ppcp_add_payment_method.ajax.create_payment_token.nonce, - vault_setup_token: vaultSetupToken, - }) - }) +import ErrorHandler from "../../../ppcp-button/resources/js/modules/ErrorHandler"; +import {cardFieldStyles} from "../../../ppcp-button/resources/js/modules/Helper/CardFieldsHelper"; - const result = await response.json() - console.log(result) - }, - onError: (error) => { - console.error(error) - } - }, - `#ppc-button-${PaymentMethods.PAYPAL}-save-payment-method` +const errorHandler = new ErrorHandler( + PayPalCommerceGateway.labels.error.generic, + document.querySelector('.woocommerce-notices-wrapper') ); const init = () => { @@ -69,6 +27,154 @@ document.addEventListener( jQuery(document.body).on('click init_add_payment_method', '.payment_methods input.input-radio', function () { init() }); + + loadScript({ + clientId: ppcp_add_payment_method.client_id, + merchantId: ppcp_add_payment_method.merchant_id, + dataUserIdToken: ppcp_add_payment_method.id_token, + components: 'buttons,card-fields', + }) + .then((paypal) => { + errorHandler.clear(); + + paypal.Buttons( + { + createVaultSetupToken: async () => { + const response = await fetch(ppcp_add_payment_method.ajax.create_setup_token.endpoint, { + method: "POST", + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + nonce: ppcp_add_payment_method.ajax.create_setup_token.nonce, + }) + }) + + const result = await response.json() + if (result.data.id) { + return result.data.id + } + + errorHandler.message(ppcp_add_payment_method.error_message); + }, + onApprove: async ({vaultSetupToken}) => { + const response = await fetch(ppcp_add_payment_method.ajax.create_payment_token.endpoint, { + method: "POST", + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + nonce: ppcp_add_payment_method.ajax.create_payment_token.nonce, + vault_setup_token: vaultSetupToken, + }) + }) + + const result = await response.json(); + if(result.success === true) { + window.location.href = ppcp_add_payment_method.payment_methods_page; + return; + } + + errorHandler.message(ppcp_add_payment_method.error_message); + }, + onError: (error) => { + console.error(error) + errorHandler.message(ppcp_add_payment_method.error_message); + } + }, + ).render(`#ppc-button-${PaymentMethods.PAYPAL}-save-payment-method`); + + const cardField = paypal.CardFields({ + createVaultSetupToken: async () => { + const response = await fetch(ppcp_add_payment_method.ajax.create_setup_token.endpoint, { + method: "POST", + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + nonce: ppcp_add_payment_method.ajax.create_setup_token.nonce, + payment_method: PaymentMethods.CARDS, + verification_method: ppcp_add_payment_method.verification_method + }) + }) + + const result = await response.json() + if (result.data.id) { + return result.data.id + } + + errorHandler.message(ppcp_add_payment_method.error_message); + }, + onApprove: async ({vaultSetupToken}) => { + const response = await fetch(ppcp_add_payment_method.ajax.create_payment_token.endpoint, { + method: "POST", + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + nonce: ppcp_add_payment_method.ajax.create_payment_token.nonce, + vault_setup_token: vaultSetupToken, + payment_method: PaymentMethods.CARDS + }) + }) + + const result = await response.json(); + if(result.success === true) { + window.location.href = ppcp_add_payment_method.payment_methods_page; + return; + } + + errorHandler.message(ppcp_add_payment_method.error_message); + }, + onError: (error) => { + console.error(error) + errorHandler.message(ppcp_add_payment_method.error_message); + } + }); + + if (cardField.isEligible()) { + const nameField = document.getElementById('ppcp-credit-card-gateway-card-name'); + if (nameField) { + let styles = cardFieldStyles(nameField); + cardField.NameField({style: {'input': styles}}).render(nameField.parentNode); + nameField.hidden = true; + } + + const numberField = document.getElementById('ppcp-credit-card-gateway-card-number'); + if (numberField) { + let styles = cardFieldStyles(numberField); + cardField.NumberField({style: {'input': styles}}).render(numberField.parentNode); + numberField.hidden = true; + } + + const expiryField = document.getElementById('ppcp-credit-card-gateway-card-expiry'); + if (expiryField) { + let styles = cardFieldStyles(expiryField); + cardField.ExpiryField({style: {'input': styles}}).render(expiryField.parentNode); + expiryField.hidden = true; + } + + const cvvField = document.getElementById('ppcp-credit-card-gateway-card-cvc'); + if (cvvField) { + let styles = cardFieldStyles(cvvField); + cardField.CVVField({style: {'input': styles}}).render(cvvField.parentNode); + cvvField.hidden = true; + } + } + + document.querySelector('#place_order').addEventListener("click", (event) => { + event.preventDefault(); + + cardField.submit() + .catch((error) => { + console.error(error) + }); + }); + }) } ); diff --git a/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php index 151c9741c..7ed30b8eb 100644 --- a/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php +++ b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php @@ -15,6 +15,8 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface; use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData; use WooCommerce\PayPalCommerce\SavePaymentMethods\WooCommercePaymentTokens; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; /** * Class CreatePaymentToken @@ -98,16 +100,38 @@ public function handle_request(): bool { if ( is_user_logged_in() && isset( $result->customer->id ) ) { update_user_meta( get_current_user_id(), '_ppcp_target_customer_id', $result->customer->id ); - $email = ''; - if ( isset( $result->payment_source->paypal->email_address ) ) { - $email = $result->payment_source->paypal->email_address; + $payment_method = $data['payment_method'] ?? ''; + if ( $payment_method === PayPalGateway::ID ) { + $email = ''; + if ( isset( $result->payment_source->paypal->email_address ) ) { + $email = $result->payment_source->paypal->email_address; + } + + $this->wc_payment_tokens->create_payment_token_paypal( + get_current_user_id(), + $result->id, + $email + ); } - $this->wc_payment_tokens->create_payment_token_paypal( - get_current_user_id(), - $result->id, - $email - ); + if ( $payment_method === CreditCardGateway::ID ) { + $token = new \WC_Payment_Token_CC(); + $token->set_token( $result->id ); + $token->set_user_id( get_current_user_id() ); + $token->set_gateway_id( CreditCardGateway::ID ); + + $token->set_last4( $result->payment_source->card->last_digits ?? '' ); + $expiry = explode( '-', $result->payment_source->card->expiry ?? '' ); + $token->set_expiry_year( $expiry[0] ?? '' ); + $token->set_expiry_month( $expiry[1] ?? '' ); + + $brand = $result->payment_source->card->brand ?? __( 'N/A', 'woocommerce-paypal-payments' ); + if ( $brand ) { + $token->set_card_type( $brand ); + } + + $token->save(); + } } wp_send_json_success( $result ); diff --git a/modules/ppcp-save-payment-methods/src/Endpoint/CreateSetupToken.php b/modules/ppcp-save-payment-methods/src/Endpoint/CreateSetupToken.php index 1490e61b6..7eb3a5ace 100644 --- a/modules/ppcp-save-payment-methods/src/Endpoint/CreateSetupToken.php +++ b/modules/ppcp-save-payment-methods/src/Endpoint/CreateSetupToken.php @@ -14,6 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface; use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; /** * Class CreateSetupToken @@ -67,13 +68,8 @@ public static function nonce(): string { */ public function handle_request(): bool { try { - $this->request_data->read_request( $this->nonce() ); + $data = $this->request_data->read_request( $this->nonce() ); - /** - * Suppress ArgumentTypeCoercion - * - * @psalm-suppress ArgumentTypeCoercion - */ $payment_source = new PaymentSource( 'paypal', (object) array( @@ -85,6 +81,28 @@ public function handle_request(): bool { ) ); + $payment_method = $data['payment_method'] ?? ''; + if ( $payment_method === CreditCardGateway::ID ) { + $properties = (object) array(); + + $verification_method = $data['verification_method'] ?? ''; + if ( $verification_method === 'SCA_WHEN_REQUIRED' || $verification_method === 'SCA_ALWAYS' ) { + $properties = (object) array( + 'verification_method' => $verification_method, + 'usage_type' => 'MERCHANT', + 'experience_context' => (object) array( + 'return_url' => esc_url( wc_get_account_endpoint_url( 'payment-methods' ) ), + 'cancel_url' => esc_url( wc_get_account_endpoint_url( 'add-payment-method' ) ), + ), + ); + } + + $payment_source = new PaymentSource( + 'card', + $properties + ); + } + $result = $this->payment_method_tokens_endpoint->setup_tokens( $payment_source ); wp_send_json_success( $result ); diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php index 8b7be4f21..9dcb02359 100644 --- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php +++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php @@ -28,6 +28,7 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; /** * Class SavePaymentMethodsModule @@ -218,14 +219,21 @@ function() use ( $c ) { $id_token = $api->id_token( $target_customer_id ); + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + $verification_method = $settings->has( '3d_secure_contingency' ) ? $settings->get( '3d_secure_contingency' ) : ''; + wp_localize_script( 'ppcp-add-payment-method', 'ppcp_add_payment_method', array( - 'client_id' => $c->get( 'button.client_id' ), - 'merchant_id' => $c->get( 'api.merchant_id' ), - 'id_token' => $id_token, - 'ajax' => array( + 'client_id' => $c->get( 'button.client_id' ), + 'merchant_id' => $c->get( 'api.merchant_id' ), + 'id_token' => $id_token, + 'payment_methods_page' => wc_get_account_endpoint_url( 'payment-methods' ), + 'error_message' => __( 'Could not save payment method.', 'woocommerce-paypal-payments' ), + 'verification_method' => $verification_method, + 'ajax' => array( 'create_setup_token' => array( 'endpoint' => \WC_AJAX::get_endpoint( CreateSetupToken::ENDPOINT ), 'nonce' => wp_create_nonce( CreateSetupToken::nonce() ), diff --git a/modules/ppcp-save-payment-methods/yarn.lock b/modules/ppcp-save-payment-methods/yarn.lock index 0b4549a0b..2171c2b89 100644 --- a/modules/ppcp-save-payment-methods/yarn.lock +++ b/modules/ppcp-save-payment-methods/yarn.lock @@ -1031,6 +1031,13 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@paypal/paypal-js@^6.0.0": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@paypal/paypal-js/-/paypal-js-6.0.1.tgz#5d68d5863a5176383fee9424bc944231668fcffd" + integrity sha512-bvYetmkg2GEC6onsUJQx1E9hdAJWff2bS3IPeiZ9Sh9U7h26/fIgMKm240cq/908sbSoDjHys75XXd8at9OpQA== + dependencies: + promise-polyfill "^8.3.0" + "@types/eslint-scope@^3.7.3": version "3.7.5" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.5.tgz#e28b09dbb1d9d35fdfa8a884225f00440dfc5a3e" @@ -1868,6 +1875,11 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +promise-polyfill@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63" + integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg== + punycode@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"