diff --git a/.babelrc b/.babelrc
deleted file mode 100644
index 47de40687..000000000
--- a/.babelrc
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "presets": [
- "es2015",
- "stage-2"
- ],
- "plugins": [
- "add-module-exports"
- ]
-}
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 000000000..396ebe45e
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,13 @@
+!.eslintrc.js
+build
+assets
+docker
+bin
+docs
+includes
+languages
+release
+tasks
+templates
+vendor
+tests
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index 8f98772fd..000000000
--- a/.eslintrc
+++ /dev/null
@@ -1,156 +0,0 @@
-{
- "env": {
- "browser": true
- },
- "globals": {
- "_": false,
- "Backbone": false,
- "jQuery": false,
- "wp": false
- },
- "rules": {
- "accessor-pairs": [2],
- "block-scoped-var": [2],
- "callback-return": [2],
- "complexity": [2, 3],
- "consistent-return": [2],
- "consistent-this": [2, "self"],
- "constructor-super": [2],
- "default-case": [2],
- "eqeqeq": [2],
- "func-style": [0],
- "global-require": [2],
- "guard-for-in": [0],
- "handle-callback-err": [2, "^err(or)?$"],
- "id-length": [0],
- "id-match": [0],
- "indent": ["error", "tab"],
- "init-declarations": [0],
- "max-depth": [2, 3],
- "max-nested-callbacks": [2, 3],
- "max-params": [2, 4],
- "max-statements": [0],
- "new-parens": [0],
- "no-alert": [2],
- "no-array-constructor": [0],
- "no-bitwise": [0],
- "no-caller": [2],
- "no-case-declarations": [2],
- "no-catch-shadow": [2],
- "no-class-assign": [2],
- "no-cond-assign": [2],
- "no-console": [0],
- "no-const-assign": [2],
- "no-constant-condition": [0],
- "no-continue": [0],
- "no-control-regex": [2],
- "no-debugger": [2],
- "no-delete-var": [2],
- "no-div-regex": [0],
- "no-dupe-args": [2],
- "no-dupe-class-members": [2],
- "no-dupe-keys": [2],
- "no-duplicate-case": [2],
- "no-else-return": [0],
- "no-empty-character-class": [2],
- "no-empty-pattern": [2],
- "no-empty": [2],
- "no-eq-null": [2],
- "no-eval": [2],
- "no-ex-assign": [2],
- "no-extend-native": [0],
- "no-extra-bind": [2],
- "no-extra-boolean-cast": [2],
- "no-extra-parens": [2],
- "no-extra-semi": [2],
- "no-fallthrough": [2],
- "no-floating-decimal": [2],
- "no-func-assign": [2],
- "no-implicit-coercion": [2],
- "no-implicit-globals": [0],
- "no-implied-eval": [2],
- "no-inline-comments": [0],
- "no-inner-declarations": [2],
- "no-invalid-regexp": [2],
- "no-invalid-this": [0],
- "no-irregular-whitespace": [2],
- "no-iterator": [2],
- "no-label-var": [2],
- "no-labels": [0],
- "no-lone-blocks": [2],
- "no-lonely-if": [2],
- "no-loop-func": [2],
- "no-magic-numbers": [2, { "ignoreArrayIndexes": true, "ignore": [ -1, 0 ] }],
- "no-mixed-requires": [0],
- "no-multi-str": [2],
- "no-native-reassign": [2],
- "no-negated-condition": [0],
- "no-negated-in-lhs": [2],
- "no-nested-ternary": [0],
- "no-new-func": [0],
- "no-new-object": [2],
- "no-new-require": [0],
- "no-new-wrappers": [2],
- "no-new": [2],
- "no-obj-calls": [2],
- "no-octal-escape": [2],
- "no-octal": [2],
- "no-param-reassign": [2],
- "no-path-concat": [2],
- "no-plusplus": [0],
- "no-process-env": [2],
- "no-process-exit": [0],
- "no-proto": [2],
- "no-redeclare": [2],
- "no-regex-spaces": [0],
- "no-restricted-imports": [0],
- "no-restricted-syntax": [0],
- "no-return-assign": [2],
- "no-script-url": [0],
- "no-self-compare": [2],
- "no-sequences": [2],
- "no-shadow-restricted-names": [2],
- "no-shadow": [2],
- "no-sparse-arrays": [2],
- "no-sync": [0],
- "no-ternary": [0],
- "no-trailing-spaces": [2],
- "no-this-before-super": [2],
- "no-throw-literal": [2],
- "no-undef-init": [0],
- "no-undef": [2],
- "no-undefined": [0],
- "no-unneeded-ternary": [2],
- "no-unreachable": [2],
- "no-unused-expressions": [2],
- "no-unused-vars": [2],
- "no-use-before-define": [0],
- "no-useless-call": [2],
- "no-useless-concat": [2],
- "no-var": [0],
- "no-void": [0],
- "no-with": [2],
- "object-shorthand": [0],
- "one-var": [ 2, "always" ],
- "operator-assignment": [2, "always"],
- "prefer-arrow-callback": [0],
- "prefer-const": [0],
- "prefer-reflect": [0],
- "prefer-rest-params": [0],
- "prefer-spread": [0],
- "prefer-template": [0],
- "quotes": [0],
- "radix": [2, "always"],
- "require-yield": [0],
- "sort-imports": [0],
- "sort-vars": [0],
- "strict": [2, "function"],
- "use-isnan": [2],
- "valid-typeof": [2],
- "valid-jsdoc": [2],
- "vars-on-top": [0],
- "wrap-iife": [2, "inside"],
- "wrap-regex": [0],
- "yoda": [0]
- }
-}
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 000000000..c23f1382f
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,22 @@
+module.exports = {
+ extends: [ 'plugin:@woocommerce/eslint-plugin/recommended' ],
+ globals: {
+ _: false,
+ Backbone: false,
+ jQuery: false,
+ wp: false,
+ },
+ settings: {
+ jsdoc: { mode: 'typescript' },
+ // List of modules that are externals in our webpack config.
+ // This helps the `import/no-extraneous-dependencies` and
+ //`import/no-unresolved` rules account for them.
+ 'import/core-modules': [
+ '@woocommerce/blocks-registry',
+ '@woocommerce/settings',
+ '@wordpress/i18n',
+ '@wordpress/is-shallow-equal',
+ '@wordpress/element',
+ ],
+ },
+};
diff --git a/.gitattributes b/.gitattributes
index 77df73a4e..fea23a38b 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -11,7 +11,7 @@ package-lock.json export-ignore
composer.json export-ignore
phpunit.xml.dist export-ignore
phpunit.xml export-ignore
-.eslintrc export-ignore
+.eslintrc.js export-ignore
.eslintignore export-ignore
.tx export-ignore
.editorconfig export-ignore
@@ -19,7 +19,9 @@ phpunit.xml export-ignore
.phpcs.xml export-ignore
.travis.yml export-ignore
.editorconfig export-ignore
+.prettierrc.js export-ignore
.babelrc export-ignore
+webpack.config.js export-ignore
docs export-ignore
wordpress_org_assets export-ignore
assets/css/*.scss export-ignore
diff --git a/.gitignore b/.gitignore
index ca46476b5..a96235de7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ sftp-config.json
.idea/
vendor
release/
+build/
woocommerce-gateway-stripe.zip
# Ignore all log files except for .htaccess
diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 000000000..51b8aeb41
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1,3 @@
+// Import the default config file and expose it in the project root.
+// Useful for editor integrations.
+module.exports = require( '@wordpress/prettier-config' );
diff --git a/assets/css/stripe-styles.css b/assets/css/stripe-styles.css
index 93e97c6b5..5dff2c8b8 100644
--- a/assets/css/stripe-styles.css
+++ b/assets/css/stripe-styles.css
@@ -1 +1 @@
-.wc-stripe-elements-field,.wc-stripe-iban-element-field{border:1px solid #ddd;margin:5px 0;padding:5px;background-color:#fff;outline:0}#payment .methods li.woocommerce-SavedPaymentMethods-token label{display:inline}#payment .methods li.woocommerce-SavedPaymentMethods-new label{display:inline}#add_payment_method #payment ul.payment_methods li img.stripe-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-icon{max-width:40px;padding-left:3px;margin:0}#add_payment_method #payment ul.payment_methods li img.stripe-bancontact-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-bancontact-icon{max-height:65px;max-width:45px}#add_payment_method #payment ul.payment_methods li img.stripe-ideal-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-ideal-icon{max-height:35px}#add_payment_method #payment ul.payment_methods li img.stripe-p24-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-p24-icon{max-width:65px}#add_payment_method #payment ul.payment_methods li img.stripe-alipay-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-alipay-icon{max-width:50px}#add_payment_method #payment ul.payment_methods li img.stripe-sofort-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-sofort-icon{max-width:55px}#add_payment_method #payment ul.payment_methods li img.stripe-sepa-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-sepa-icon{max-width:50px}#add_payment_method #payment ul.payment_methods li img.stripe-multibanco-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-multibanco-icon{max-height:30px}#add_payment_method #payment ul.payment_methods li img.stripe-eps-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-eps-icon{max-height:30px}#add_payment_method #payment ul.payment_methods li img.stripe-giropay-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-giropay-icon{max-width:50px}#add_payment_method #payment ul.payment_methods li .stripe-credit-card-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-credit-card-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/credit-card.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-visa-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-visa-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/visa.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-amex-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-amex-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/amex.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-diners-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-diners-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/diners.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-discover-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-discover-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/discover.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-jcb-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-jcb-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/jcb.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-maestro-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-maestro-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/maestro.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-mastercard-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-mastercard-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/mastercard.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods .stripe-card-group,.woocommerce-checkout #payment ul.payment_methods .stripe-card-group{position:relative}.woocommerce-SavedPaymentMethods-token .stripe-source-errors .woocommerce-error{margin-top:1em;margin-bottom:2em}#wc-stripe-custom-button{display:block;width:100%}.gpay-button{background-origin:content-box;background-position:center center;background-repeat:no-repeat;background-size:contain;border:0;border-radius:4px;box-shadow:rgba(60,64,67,.3) 0 1px 1px 0,rgba(60,64,67,.15) 0 1px 3px 1px;cursor:pointer;height:40px;min-height:40px;padding:11px 24px;width:100%}.gpay-button.light{width:calc(100% - 3px);background-color:#fff}.gpay-button.light:active{background-color:#fff}.gpay-button.light:hover{background-color:#f8f8f8}.gpay-button.light:focus{box-shadow:#e8e8e8 0 1px 1px 0,#e8e8e8 0 1px 3px;outline:0}.gpay-button.dark{background-color:#000;box-shadow:none;padding:12px 24px 10px}.gpay-button.dark:active{background-color:#5f6368}.gpay-button.dark:hover{background-color:#3c4043}.gpay-button.dark:focus{box-shadow:#5f6368 0 1px 1px 0,#5f6368 0 1px 3px;outline:0}.gpay-button.short.light{background-image:url(https://www.gstatic.com/instantbuy/svg/light_gpay.svg)}.gpay-button.short.dark{background-image:url(https://www.gstatic.com/instantbuy/svg/dark_gpay.svg)}
\ No newline at end of file
+.wc-stripe-elements-field,.wc-stripe-iban-element-field{border:1px solid #ddd;margin:5px 0;padding:5px;background-color:#fff;outline:0}#payment .methods li.woocommerce-SavedPaymentMethods-token label{display:inline}#payment .methods li.woocommerce-SavedPaymentMethods-new label{display:inline}#add_payment_method #payment ul.payment_methods li img.stripe-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-icon{max-width:40px;padding-left:3px;margin:0}#add_payment_method #payment ul.payment_methods li img.stripe-bancontact-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-bancontact-icon{max-height:65px;max-width:45px}#add_payment_method #payment ul.payment_methods li img.stripe-ideal-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-ideal-icon{max-height:35px}#add_payment_method #payment ul.payment_methods li img.stripe-p24-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-p24-icon{max-width:65px}#add_payment_method #payment ul.payment_methods li img.stripe-alipay-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-alipay-icon{max-width:50px}#add_payment_method #payment ul.payment_methods li img.stripe-sofort-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-sofort-icon{max-width:55px}#add_payment_method #payment ul.payment_methods li img.stripe-sepa-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-sepa-icon{max-width:50px}#add_payment_method #payment ul.payment_methods li img.stripe-multibanco-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-multibanco-icon{max-height:30px}#add_payment_method #payment ul.payment_methods li img.stripe-eps-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-eps-icon{max-height:30px}#add_payment_method #payment ul.payment_methods li img.stripe-giropay-icon,.woocommerce-checkout #payment ul.payment_methods li img.stripe-giropay-icon{max-width:50px}#add_payment_method #payment ul.payment_methods li .stripe-credit-card-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-credit-card-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/credit-card.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-visa-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-visa-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/visa.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-amex-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-amex-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/amex.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-diners-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-diners-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/diners.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-discover-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-discover-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/discover.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-jcb-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-jcb-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/jcb.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-maestro-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-maestro-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/maestro.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods li .stripe-mastercard-brand,.woocommerce-checkout #payment ul.payment_methods li .stripe-mastercard-brand{position:absolute;top:50%;margin-top:-10px;right:10px;background:no-repeat url(../images/mastercard.svg);display:block;width:30px;height:24px}#add_payment_method #payment ul.payment_methods .stripe-card-group,.woocommerce-checkout #payment ul.payment_methods .stripe-card-group{position:relative}.woocommerce-SavedPaymentMethods-token .stripe-source-errors .woocommerce-error{margin-top:1em;margin-bottom:2em}#wc-stripe-custom-button{display:block;width:100%}.gpay-button{background-origin:content-box;background-position:center center;background-repeat:no-repeat;background-size:contain;border:0;border-radius:4px;cursor:pointer;height:40px;min-height:40px;padding:11px 24px;width:100%}.gpay-button.light{width:calc(100% - 3px);background-color:#fff;margin:2px 0 3px 0}.gpay-button.light:active{background-color:#fff}.gpay-button.light:hover{background-color:#f8f8f8}.gpay-button.light:focus{box-shadow:#e8e8e8 0 1px 1px 0,#e8e8e8 0 1px 3px;outline:0}.gpay-button.light-outline{width:calc(100% - 3px);background-color:#fff;box-shadow:rgba(60,64,67,.3) 0 1px 1px 0,rgba(60,64,67,.15) 0 1px 3px 1px;margin:2px 0 3px 0}.gpay-button.light-outline:active{background-color:#fff}.gpay-button.light-outline:hover{background-color:#f8f8f8}.gpay-button.light-outline:focus{box-shadow:#e8e8e8 0 1px 1px 0,#e8e8e8 0 1px 3px;outline:0}.gpay-button.dark{background-color:#000;padding:12px 24px 10px}.gpay-button.dark:active{background-color:#5f6368}.gpay-button.dark:hover{background-color:#3c4043}.gpay-button.dark:focus{box-shadow:#5f6368 0 1px 1px 0,#5f6368 0 1px 3px;outline:0}.gpay-button.short.light,.gpay-button.short.light-outline{background-image:url(https://www.gstatic.com/instantbuy/svg/light_gpay.svg)}.gpay-button.short.dark{background-image:url(https://www.gstatic.com/instantbuy/svg/dark_gpay.svg)}
\ No newline at end of file
diff --git a/assets/css/stripe-styles.scss b/assets/css/stripe-styles.scss
index ef9e8a445..7a7cb32ea 100755
--- a/assets/css/stripe-styles.scss
+++ b/assets/css/stripe-styles.scss
@@ -1,76 +1,191 @@
-.wc-stripe-elements-field, .wc-stripe-iban-element-field { border:1px solid #ddd; margin:5px 0; padding:5px; background-color:#fff; outline:0; }
-#payment .methods li.woocommerce-SavedPaymentMethods-token label { display: inline; }
-#payment .methods li.woocommerce-SavedPaymentMethods-new label { display: inline; }
+.wc-stripe-elements-field,
+.wc-stripe-iban-element-field {
+ border: 1px solid #ddd;
+ margin: 5px 0;
+ padding: 5px;
+ background-color: #fff;
+ outline: 0;
+}
+#payment .methods li.woocommerce-SavedPaymentMethods-token label {
+ display: inline;
+}
+#payment .methods li.woocommerce-SavedPaymentMethods-new label {
+ display: inline;
+}
.woocommerce-checkout #payment ul.payment_methods li img.stripe-icon,
-#add_payment_method #payment ul.payment_methods li img.stripe-icon { max-width: 40px; padding-left: 3px; margin: 0; }
+#add_payment_method #payment ul.payment_methods li img.stripe-icon {
+ max-width: 40px;
+ padding-left: 3px;
+ margin: 0;
+}
.woocommerce-checkout #payment ul.payment_methods li img.stripe-bancontact-icon,
-#add_payment_method #payment ul.payment_methods li img.stripe-bancontact-icon { max-height: 65px; max-width: 45px; }
+#add_payment_method #payment ul.payment_methods li img.stripe-bancontact-icon {
+ max-height: 65px;
+ max-width: 45px;
+}
.woocommerce-checkout #payment ul.payment_methods li img.stripe-ideal-icon,
-#add_payment_method #payment ul.payment_methods li img.stripe-ideal-icon { max-height: 35px; }
+#add_payment_method #payment ul.payment_methods li img.stripe-ideal-icon {
+ max-height: 35px;
+}
.woocommerce-checkout #payment ul.payment_methods li img.stripe-p24-icon,
-#add_payment_method #payment ul.payment_methods li img.stripe-p24-icon { max-width: 65px; }
+#add_payment_method #payment ul.payment_methods li img.stripe-p24-icon {
+ max-width: 65px;
+}
.woocommerce-checkout #payment ul.payment_methods li img.stripe-alipay-icon,
-#add_payment_method #payment ul.payment_methods li img.stripe-alipay-icon { max-width: 50px; }
+#add_payment_method #payment ul.payment_methods li img.stripe-alipay-icon {
+ max-width: 50px;
+}
.woocommerce-checkout #payment ul.payment_methods li img.stripe-sofort-icon,
-#add_payment_method #payment ul.payment_methods li img.stripe-sofort-icon { max-width: 55px; }
+#add_payment_method #payment ul.payment_methods li img.stripe-sofort-icon {
+ max-width: 55px;
+}
.woocommerce-checkout #payment ul.payment_methods li img.stripe-sepa-icon,
-#add_payment_method #payment ul.payment_methods li img.stripe-sepa-icon { max-width: 50px; }
+#add_payment_method #payment ul.payment_methods li img.stripe-sepa-icon {
+ max-width: 50px;
+}
.woocommerce-checkout #payment ul.payment_methods li img.stripe-multibanco-icon,
-#add_payment_method #payment ul.payment_methods li img.stripe-multibanco-icon { max-height: 30px; }
+#add_payment_method #payment ul.payment_methods li img.stripe-multibanco-icon {
+ max-height: 30px;
+}
.woocommerce-checkout #payment ul.payment_methods li img.stripe-eps-icon,
-#add_payment_method #payment ul.payment_methods li img.stripe-eps-icon { max-height: 30px; }
+#add_payment_method #payment ul.payment_methods li img.stripe-eps-icon {
+ max-height: 30px;
+}
.woocommerce-checkout #payment ul.payment_methods li img.stripe-giropay-icon,
-#add_payment_method #payment ul.payment_methods li img.stripe-giropay-icon { max-width: 50px; }
+#add_payment_method #payment ul.payment_methods li img.stripe-giropay-icon {
+ max-width: 50px;
+}
.woocommerce-checkout #payment ul.payment_methods li .stripe-credit-card-brand,
-#add_payment_method #payment ul.payment_methods li .stripe-credit-card-brand { position: absolute; top: 50%; margin-top: -10px; right: 10px; background: no-repeat url( '../images/credit-card.svg' ); display: block; width: 30px; height: 24px; }
+#add_payment_method #payment ul.payment_methods li .stripe-credit-card-brand {
+ position: absolute;
+ top: 50%;
+ margin-top: -10px;
+ right: 10px;
+ background: no-repeat url( '../images/credit-card.svg' );
+ display: block;
+ width: 30px;
+ height: 24px;
+}
.woocommerce-checkout #payment ul.payment_methods li .stripe-visa-brand,
-#add_payment_method #payment ul.payment_methods li .stripe-visa-brand { position: absolute; top: 50%; margin-top: -10px; right: 10px; background: no-repeat url( '../images/visa.svg' ); display: block; width: 30px; height: 24px; }
+#add_payment_method #payment ul.payment_methods li .stripe-visa-brand {
+ position: absolute;
+ top: 50%;
+ margin-top: -10px;
+ right: 10px;
+ background: no-repeat url( '../images/visa.svg' );
+ display: block;
+ width: 30px;
+ height: 24px;
+}
.woocommerce-checkout #payment ul.payment_methods li .stripe-amex-brand,
-#add_payment_method #payment ul.payment_methods li .stripe-amex-brand { position: absolute; top: 50%; margin-top: -10px; right: 10px; background: no-repeat url( '../images/amex.svg' ); display: block; width: 30px; height: 24px; }
+#add_payment_method #payment ul.payment_methods li .stripe-amex-brand {
+ position: absolute;
+ top: 50%;
+ margin-top: -10px;
+ right: 10px;
+ background: no-repeat url( '../images/amex.svg' );
+ display: block;
+ width: 30px;
+ height: 24px;
+}
.woocommerce-checkout #payment ul.payment_methods li .stripe-diners-brand,
-#add_payment_method #payment ul.payment_methods li .stripe-diners-brand { position: absolute; top: 50%; margin-top: -10px; right: 10px; background: no-repeat url( '../images/diners.svg' ); display: block; width: 30px; height: 24px; }
+#add_payment_method #payment ul.payment_methods li .stripe-diners-brand {
+ position: absolute;
+ top: 50%;
+ margin-top: -10px;
+ right: 10px;
+ background: no-repeat url( '../images/diners.svg' );
+ display: block;
+ width: 30px;
+ height: 24px;
+}
.woocommerce-checkout #payment ul.payment_methods li .stripe-discover-brand,
-#add_payment_method #payment ul.payment_methods li .stripe-discover-brand { position: absolute; top: 50%; margin-top: -10px; right: 10px; background: no-repeat url( '../images/discover.svg' ); display: block; width: 30px; height: 24px; }
+#add_payment_method #payment ul.payment_methods li .stripe-discover-brand {
+ position: absolute;
+ top: 50%;
+ margin-top: -10px;
+ right: 10px;
+ background: no-repeat url( '../images/discover.svg' );
+ display: block;
+ width: 30px;
+ height: 24px;
+}
.woocommerce-checkout #payment ul.payment_methods li .stripe-jcb-brand,
-#add_payment_method #payment ul.payment_methods li .stripe-jcb-brand { position: absolute; top: 50%; margin-top: -10px; right: 10px; background: no-repeat url( '../images/jcb.svg' ); display: block; width: 30px; height: 24px; }
+#add_payment_method #payment ul.payment_methods li .stripe-jcb-brand {
+ position: absolute;
+ top: 50%;
+ margin-top: -10px;
+ right: 10px;
+ background: no-repeat url( '../images/jcb.svg' );
+ display: block;
+ width: 30px;
+ height: 24px;
+}
.woocommerce-checkout #payment ul.payment_methods li .stripe-maestro-brand,
-#add_payment_method #payment ul.payment_methods li .stripe-maestro-brand { position: absolute; top: 50%; margin-top: -10px; right: 10px; background: no-repeat url( '../images/maestro.svg' ); display: block; width: 30px; height: 24px; }
+#add_payment_method #payment ul.payment_methods li .stripe-maestro-brand {
+ position: absolute;
+ top: 50%;
+ margin-top: -10px;
+ right: 10px;
+ background: no-repeat url( '../images/maestro.svg' );
+ display: block;
+ width: 30px;
+ height: 24px;
+}
.woocommerce-checkout #payment ul.payment_methods li .stripe-mastercard-brand,
-#add_payment_method #payment ul.payment_methods li .stripe-mastercard-brand { position: absolute; top: 50%; margin-top: -10px; right: 10px; background: no-repeat url( '../images/mastercard.svg' ); display: block; width: 30px; height: 24px; }
+#add_payment_method #payment ul.payment_methods li .stripe-mastercard-brand {
+ position: absolute;
+ top: 50%;
+ margin-top: -10px;
+ right: 10px;
+ background: no-repeat url( '../images/mastercard.svg' );
+ display: block;
+ width: 30px;
+ height: 24px;
+}
.woocommerce-checkout #payment ul.payment_methods .stripe-card-group,
-#add_payment_method #payment ul.payment_methods .stripe-card-group { position: relative; }
+#add_payment_method #payment ul.payment_methods .stripe-card-group {
+ position: relative;
+}
-.woocommerce-SavedPaymentMethods-token .stripe-source-errors .woocommerce-error { margin-top: 1em; margin-bottom: 2em; }
+.woocommerce-SavedPaymentMethods-token
+ .stripe-source-errors
+ .woocommerce-error {
+ margin-top: 1em;
+ margin-bottom: 2em;
+}
-#wc-stripe-custom-button { display: block; width: 100%; }
+#wc-stripe-custom-button {
+ display: block;
+ width: 100%;
+}
.gpay-button {
background-origin: content-box;
background-position: center center;
background-repeat: no-repeat;
background-size: contain;
- border: 0px;
+ border: 0;
border-radius: 4px;
- box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 1px 0px, rgba(60, 64, 67, 0.15) 0px 1px 3px 1px;
cursor: pointer;
height: 40px;
min-height: 40px;
@@ -81,6 +196,34 @@
// account for button box-shadow when setting width
width: calc( 100% - 3px );
background-color: #fff;
+
+ // Add vertical margin to make box-shadow visible in blocks.
+ margin: 2px 0 3px 0;
+
+ &:active {
+ background-color: #fff;
+ }
+ &:hover {
+ background-color: #f8f8f8;
+ }
+ &:focus {
+ box-shadow: #e8e8e8 0 1px 1px 0, #e8e8e8 0 1px 3px;
+ outline: 0;
+ }
+ }
+
+ &.light-outline {
+ // account for button box-shadow when setting width
+ width: calc( 100% - 3px );
+ background-color: #fff;
+
+ // The box-shadow forms the outline and as such is always on.
+ box-shadow: rgba( 60, 64, 67, 0.3 ) 0px 1px 1px 0,
+ rgba( 60, 64, 67, 0.15 ) 0 1px 3px 1px;
+
+ // Add vertical margin to make box-shadow visible in blocks.
+ margin: 2px 0 3px 0;
+
&:active {
background-color: #fff;
}
@@ -95,12 +238,11 @@
&.dark {
background-color: #000;
- box-shadow: none;
padding: 12px 24px 10px;
&:active {
background-color: #5f6368;
}
- &:hover{
+ &:hover {
background-color: #3c4043;
}
&:focus {
@@ -110,11 +252,12 @@
}
&.short {
- &.light {
- background-image: url(https://www.gstatic.com/instantbuy/svg/light_gpay.svg);
+ &.light,
+ &.light-outline {
+ background-image: url( https://www.gstatic.com/instantbuy/svg/light_gpay.svg );
}
&.dark {
- background-image: url(https://www.gstatic.com/instantbuy/svg/dark_gpay.svg);
+ background-image: url( https://www.gstatic.com/instantbuy/svg/dark_gpay.svg );
}
}
}
diff --git a/assets/js/stripe-payment-request.js b/assets/js/stripe-payment-request.js
index b7980f115..6c72aa264 100644
--- a/assets/js/stripe-payment-request.js
+++ b/assets/js/stripe-payment-request.js
@@ -2,7 +2,9 @@
jQuery( function( $ ) {
'use strict';
- var stripe = Stripe( wc_stripe_payment_request_params.stripe.key ),
+ var stripe = Stripe( wc_stripe_payment_request_params.stripe.key, {
+ locale: wc_stripe_params.stripe_locale || 'auto',
+ } ),
paymentRequestType;
/**
@@ -542,7 +544,7 @@ jQuery( function( $ ) {
},
createGooglePayButton: function () {
- var allowedThemes = [ 'dark', 'light' ];
+ var allowedThemes = [ 'dark', 'light', 'light-outline' ];
var allowedTypes = [ 'short', 'long' ];
var theme = wc_stripe_payment_request_params.button.theme;
@@ -550,14 +552,15 @@ jQuery( function( $ ) {
var locale = wc_stripe_payment_request_params.button.locale;
var height = wc_stripe_payment_request_params.button.height;
theme = allowedThemes.includes( theme ) ? theme : 'light';
+ var gpaySvgTheme = 'dark' === theme ? 'dark' : 'light';
type = allowedTypes.includes( type ) ? type : 'long';
var button = $( '' );
button.css( 'height', height + 'px' );
button.addClass( theme + ' ' + type );
if ( 'long' === type ) {
- var url = 'https://www.gstatic.com/instantbuy/svg/' + theme + '/' + locale + '.svg';
- var fallbackUrl = 'https://www.gstatic.com/instantbuy/svg/' + theme + '/en.svg';
+ var url = 'https://www.gstatic.com/instantbuy/svg/' + gpaySvgTheme + '/' + locale + '.svg';
+ var fallbackUrl = 'https://www.gstatic.com/instantbuy/svg/' + gpaySvgTheme + '/en.svg';
// Check if locale GPay button exists, default to en if not
setBackgroundImageWithFallback( button, url, fallbackUrl );
}
@@ -609,11 +612,11 @@ jQuery( function( $ ) {
paymentRequest.show();
}
});
-
+
$( document.body ).on( 'wc_stripe_unblock_payment_request_button wc_stripe_enable_payment_request_button', function () {
wc_stripe_payment_request.unblockPaymentRequestButton();
} );
-
+
$( document.body ).on( 'wc_stripe_block_payment_request_button', function () {
wc_stripe_payment_request.blockPaymentRequestButton( 'wc_request_button_is_blocked' );
} );
diff --git a/assets/js/stripe-payment-request.min.js b/assets/js/stripe-payment-request.min.js
index 48983d988..263e5ece1 100644
--- a/assets/js/stripe-payment-request.min.js
+++ b/assets/js/stripe-payment-request.min.js
@@ -1 +1 @@
-jQuery(function(i){"use strict";var o,u=Stripe(wc_stripe_payment_request_params.stripe.key),p={getAjaxURL:function(t){return wc_stripe_payment_request_params.ajax_url.toString().replace("%%endpoint%%","wc_stripe_"+t)},getCartDetails:function(){var t={security:wc_stripe_payment_request_params.nonce.payment};i.ajax({type:"POST",data:t,url:p.getAjaxURL("get_cart_details"),success:function(t){p.startPaymentRequest(t)}})},getAttributes:function(){var t=i(".variations_form").find(".variations select"),a={},n=0,r=0;return t.each(function(){var t=i(this).data("attribute_name")||i(this).attr("name"),e=i(this).val()||"";0').text(t)},displayErrorMessage:function(t){if(i(".woocommerce-error").remove(),wc_stripe_payment_request_params.is_product_page){var e=i(".product").first();e.before(t),i("html, body").animate({scrollTop:e.prev(".woocommerce-error").offset().top},600)}else{var a=i(".shop_table.cart").closest("form");a.before(t),i("html, body").animate({scrollTop:a.prev(".woocommerce-error").offset().top},600)}},abortPayment:function(t,e){t.complete("fail"),p.displayErrorMessage(e)},completePayment:function(t,e){p.block(),t.complete("success"),window.location=e},block:function(){i.blockUI({message:null,overlayCSS:{background:"#fff",opacity:.6}})},updateShippingOptions:function(t,e){var a={security:wc_stripe_payment_request_params.nonce.shipping,country:e.country,state:e.region,postcode:e.postalCode,city:e.city,address:void 0===e.addressLine[0]?"":e.addressLine[0],address_2:void 0===e.addressLine[1]?"":e.addressLine[1],payment_request_type:o,is_product_page:wc_stripe_payment_request_params.is_product_page};return i.ajax({type:"POST",data:a,url:p.getAjaxURL("get_shipping_options")})},updateShippingDetails:function(t,e){var a={security:wc_stripe_payment_request_params.nonce.update_shipping,shipping_method:[e.id],payment_request_type:o,is_product_page:wc_stripe_payment_request_params.is_product_page};return i.ajax({type:"POST",data:a,url:p.getAjaxURL("update_shipping_method")})},addToCart:function(){var t=i(".single_add_to_cart_button").val();i(".single_variation_wrap").length&&(t=i(".single_variation_wrap").find('input[name="product_id"]').val());var n={security:wc_stripe_payment_request_params.nonce.add_to_cart,product_id:t,qty:i(".quantity .qty").val(),attributes:i(".variations_form").length?p.getAttributes().data:[]},e=i("form.cart").serializeArray();return i.each(e,function(t,e){if(/^addon-/.test(e.name))if(/\[\]$/.test(e.name)){var a=e.name.substring(0,e.name.length-2);n[a]?n[a].push(e.value):n[a]=[e.value]}else n[e.name]=e.value}),i.ajax({type:"POST",data:n,url:p.getAjaxURL("add_to_cart")})},clearCart:function(){var t={security:wc_stripe_payment_request_params.nonce.clear_cart};return i.ajax({type:"POST",data:t,url:p.getAjaxURL("clear_cart"),success:function(t){}})},getRequestOptionsFromLocal:function(){return{total:wc_stripe_payment_request_params.product.total,currency:wc_stripe_payment_request_params.checkout.currency_code,country:wc_stripe_payment_request_params.checkout.country_code,requestPayerName:!0,requestPayerEmail:!0,requestPayerPhone:wc_stripe_payment_request_params.checkout.needs_payer_phone,requestShipping:wc_stripe_payment_request_params.product.requestShipping,displayItems:wc_stripe_payment_request_params.product.displayItems}},startPaymentRequest:function(t){var a,e;a=wc_stripe_payment_request_params.is_product_page?e=p.getRequestOptionsFromLocal():(e={total:t.order_data.total,currency:t.order_data.currency,country:t.order_data.country_code,requestPayerName:!0,requestPayerEmail:!0,requestPayerPhone:wc_stripe_payment_request_params.checkout.needs_payer_phone,requestShipping:!!t.shipping_required,displayItems:t.order_data.displayItems},t.order_data),"PR"===e.country&&(e.country="US");try{var n=u.paymentRequest(e),r=u.elements({locale:wc_stripe_payment_request_params.button.locale}),s=p.createPaymentRequestButton(r,n);n.canMakePayment().then(function(t){t&&(o=t.applePay?"apple_pay":"payment_request_api",p.attachPaymentRequestButtonEventListeners(s,n),p.showPaymentRequestButton(s))}),n.on("shippingaddresschange",function(e){i.when(p.updateShippingOptions(a,e.shippingAddress)).then(function(t){e.updateWith({status:t.result,shippingOptions:t.shipping_options,total:t.total,displayItems:t.displayItems})})}),n.on("shippingoptionchange",function(e){i.when(p.updateShippingDetails(a,e.shippingOption)).then(function(t){"success"===t.result&&e.updateWith({status:"success",total:t.total,displayItems:t.displayItems}),"fail"===t.result&&e.updateWith({status:"fail"})})}),n.on("source",function(e){"no"===wc_stripe_payment_request_params.stripe.allow_prepaid_card&&"prepaid"===e.source.card.funding?p.abortPayment(e,p.getErrorMessageHTML(wc_stripe_payment_request_params.i18n.no_prepaid_card)):i.when(p.processSource(e,o)).then(function(t){"success"===t.result?p.completePayment(e,t.redirect):p.abortPayment(e,t.messages)})})}catch(t){console.error(t)}},getSelectedProductData:function(){var t=i(".single_add_to_cart_button").val();i(".single_variation_wrap").length&&(t=i(".single_variation_wrap").find('input[name="product_id"]').val());var e=(i("#product-addons-total").data("price_data")||[]).reduce(function(t,e){return t+e.cost},0),a={security:wc_stripe_payment_request_params.nonce.get_selected_product_data,product_id:t,qty:i(".quantity .qty").val(),attributes:i(".variations_form").length?p.getAttributes().data:[],addon_value:e};return i.ajax({type:"POST",data:a,url:p.getAjaxURL("get_selected_product_data")})},debounce:function(n,r,s){var i;return function(){var t=this,e=arguments,a=s&&!i;clearTimeout(i),i=setTimeout(function(){i=null,s||r.apply(t,e)},n),a&&r.apply(t,e)}},createPaymentRequestButton:function(t,e){var a;if(wc_stripe_payment_request_params.button.is_custom&&(a=i(wc_stripe_payment_request_params.button.css_selector)).length)return a.data("isCustom",!0),a;if(wc_stripe_payment_request_params.button.is_branded){if(p.shouldUseGooglePayBrand())return(a=p.createGooglePayButton()).data("isBranded",!0),a;wc_stripe_payment_request_params.button.type="long"===wc_stripe_payment_request_params.button.branded_type?"buy":"default"}return t.create("paymentRequestButton",{paymentRequest:e,style:{paymentRequestButton:{type:wc_stripe_payment_request_params.button.type,theme:wc_stripe_payment_request_params.button.theme,height:wc_stripe_payment_request_params.button.height+"px"}}})},isCustomPaymentRequestButton:function(t){return t&&"function"==typeof t.data&&t.data("isCustom")},isBrandedPaymentRequestButton:function(t){return t&&"function"==typeof t.data&&t.data("isBranded")},shouldUseGooglePayBrand:function(){var t=window.navigator.userAgent.toLowerCase(),e=/chrome/.test(t)&&!/edge|edg|opr|brave\//.test(t)&&"Google Inc."===window.navigator.vendor,a=e&&window.navigator.brave;return e&&!a},createGooglePayButton:function(){var t=wc_stripe_payment_request_params.button.theme,e=wc_stripe_payment_request_params.button.branded_type,a=wc_stripe_payment_request_params.button.locale,n=wc_stripe_payment_request_params.button.height;t=["dark","light"].includes(t)?t:"light",e=["short","long"].includes(e)?e:"long";var r=i('');r.css("height",n+"px"),r.addClass(t+" "+e),"long"===e&&function(t,e,a){t.css("background-image","url("+e+")");var n=document.createElement("img");n.onerror=function(){t.css("background-image","url("+a+")")},n.src=e}(r,"https://www.gstatic.com/instantbuy/svg/"+t+"/"+a+".svg","https://www.gstatic.com/instantbuy/svg/"+t+"/en.svg");return r},attachPaymentRequestButtonEventListeners:function(t,e){t.on("click",function(t){i("body").addClass("woocommerce-stripe-prb-clicked")}),wc_stripe_payment_request_params.is_product_page?p.attachProductPageEventListeners(t,e):p.attachCartPageEventListeners(t,e)},attachProductPageEventListeners:function(e,a){var n=[],r=i(".single_add_to_cart_button");e.on("click",function(t){return r.is(".disabled")?(t.preventDefault(),void(r.is(".wc-variation-is-unavailable")?window.alert(wc_add_to_cart_variation_params.i18n_unavailable_text):r.is(".wc-variation-selection-needed")&&window.alert(wc_add_to_cart_variation_params.i18n_make_a_selection_text))):0').text(t)},displayErrorMessage:function(t){var e;i(".woocommerce-error").remove(),wc_stripe_payment_request_params.is_product_page?((e=i(".product").first()).before(t),i("html, body").animate({scrollTop:e.prev(".woocommerce-error").offset().top},600)):((e=i(".shop_table.cart").closest("form")).before(t),i("html, body").animate({scrollTop:e.prev(".woocommerce-error").offset().top},600))},abortPayment:function(t,e){t.complete("fail"),p.displayErrorMessage(e)},completePayment:function(t,e){p.block(),t.complete("success"),window.location=e},block:function(){i.blockUI({message:null,overlayCSS:{background:"#fff",opacity:.6}})},updateShippingOptions:function(t,e){e={security:wc_stripe_payment_request_params.nonce.shipping,country:e.country,state:e.region,postcode:e.postalCode,city:e.city,address:void 0===e.addressLine[0]?"":e.addressLine[0],address_2:void 0===e.addressLine[1]?"":e.addressLine[1],payment_request_type:o,is_product_page:wc_stripe_payment_request_params.is_product_page};return i.ajax({type:"POST",data:e,url:p.getAjaxURL("get_shipping_options")})},updateShippingDetails:function(t,e){e={security:wc_stripe_payment_request_params.nonce.update_shipping,shipping_method:[e.id],payment_request_type:o,is_product_page:wc_stripe_payment_request_params.is_product_page};return i.ajax({type:"POST",data:e,url:p.getAjaxURL("update_shipping_method")})},addToCart:function(){var t=i(".single_add_to_cart_button").val();i(".single_variation_wrap").length&&(t=i(".single_variation_wrap").find('input[name="product_id"]').val());var n={security:wc_stripe_payment_request_params.nonce.add_to_cart,product_id:t,qty:i(".quantity .qty").val(),attributes:i(".variations_form").length?p.getAttributes().data:[]},t=i("form.cart").serializeArray();return i.each(t,function(t,e){var a;/^addon-/.test(e.name)&&(/\[\]$/.test(e.name)?(a=e.name.substring(0,e.name.length-2),n[a]?n[a].push(e.value):n[a]=[e.value]):n[e.name]=e.value)}),i.ajax({type:"POST",data:n,url:p.getAjaxURL("add_to_cart")})},clearCart:function(){var t={security:wc_stripe_payment_request_params.nonce.clear_cart};return i.ajax({type:"POST",data:t,url:p.getAjaxURL("clear_cart"),success:function(t){}})},getRequestOptionsFromLocal:function(){return{total:wc_stripe_payment_request_params.product.total,currency:wc_stripe_payment_request_params.checkout.currency_code,country:wc_stripe_payment_request_params.checkout.country_code,requestPayerName:!0,requestPayerEmail:!0,requestPayerPhone:wc_stripe_payment_request_params.checkout.needs_payer_phone,requestShipping:wc_stripe_payment_request_params.product.requestShipping,displayItems:wc_stripe_payment_request_params.product.displayItems}},startPaymentRequest:function(t){var e,a=wc_stripe_payment_request_params.is_product_page?e=p.getRequestOptionsFromLocal():(e={total:t.order_data.total,currency:t.order_data.currency,country:t.order_data.country_code,requestPayerName:!0,requestPayerEmail:!0,requestPayerPhone:wc_stripe_payment_request_params.checkout.needs_payer_phone,requestShipping:!!t.shipping_required,displayItems:t.order_data.displayItems},t.order_data);"PR"===e.country&&(e.country="US");try{var n=u.paymentRequest(e),r=u.elements({locale:wc_stripe_payment_request_params.button.locale}),s=p.createPaymentRequestButton(r,n);n.canMakePayment().then(function(t){t&&(o=t.applePay?"apple_pay":"payment_request_api",p.attachPaymentRequestButtonEventListeners(s,n),p.showPaymentRequestButton(s))}),n.on("shippingaddresschange",function(e){i.when(p.updateShippingOptions(a,e.shippingAddress)).then(function(t){e.updateWith({status:t.result,shippingOptions:t.shipping_options,total:t.total,displayItems:t.displayItems})})}),n.on("shippingoptionchange",function(e){i.when(p.updateShippingDetails(a,e.shippingOption)).then(function(t){"success"===t.result&&e.updateWith({status:"success",total:t.total,displayItems:t.displayItems}),"fail"===t.result&&e.updateWith({status:"fail"})})}),n.on("source",function(e){"no"===wc_stripe_payment_request_params.stripe.allow_prepaid_card&&"prepaid"===e.source.card.funding?p.abortPayment(e,p.getErrorMessageHTML(wc_stripe_payment_request_params.i18n.no_prepaid_card)):i.when(p.processSource(e,o)).then(function(t){"success"===t.result?p.completePayment(e,t.redirect):p.abortPayment(e,t.messages)})})}catch(t){console.error(t)}},getSelectedProductData:function(){var t=i(".single_add_to_cart_button").val();i(".single_variation_wrap").length&&(t=i(".single_variation_wrap").find('input[name="product_id"]').val());var e=(i("#product-addons-total").data("price_data")||[]).reduce(function(t,e){return t+e.cost},0),e={security:wc_stripe_payment_request_params.nonce.get_selected_product_data,product_id:t,qty:i(".quantity .qty").val(),attributes:i(".variations_form").length?p.getAttributes().data:[],addon_value:e};return i.ajax({type:"POST",data:e,url:p.getAjaxURL("get_selected_product_data")})},debounce:function(n,r,s){var i;return function(){var t=this,e=arguments,a=s&&!i;clearTimeout(i),i=setTimeout(function(){i=null,s||r.apply(t,e)},n),a&&r.apply(t,e)}},createPaymentRequestButton:function(t,e){var a;if(wc_stripe_payment_request_params.button.is_custom&&(a=i(wc_stripe_payment_request_params.button.css_selector)).length)return a.data("isCustom",!0),a;if(wc_stripe_payment_request_params.button.is_branded){if(p.shouldUseGooglePayBrand())return(a=p.createGooglePayButton()).data("isBranded",!0),a;wc_stripe_payment_request_params.button.type="long"===wc_stripe_payment_request_params.button.branded_type?"buy":"default"}return t.create("paymentRequestButton",{paymentRequest:e,style:{paymentRequestButton:{type:wc_stripe_payment_request_params.button.type,theme:wc_stripe_payment_request_params.button.theme,height:wc_stripe_payment_request_params.button.height+"px"}}})},isCustomPaymentRequestButton:function(t){return t&&"function"==typeof t.data&&t.data("isCustom")},isBrandedPaymentRequestButton:function(t){return t&&"function"==typeof t.data&&t.data("isBranded")},shouldUseGooglePayBrand:function(){var t=window.navigator.userAgent.toLowerCase(),e=/chrome/.test(t)&&!/edge|edg|opr|brave\//.test(t)&&"Google Inc."===window.navigator.vendor,t=e&&window.navigator.brave;return e&&!t},createGooglePayButton:function(){var t=wc_stripe_payment_request_params.button.theme,e=wc_stripe_payment_request_params.button.branded_type,a=wc_stripe_payment_request_params.button.locale,n=wc_stripe_payment_request_params.button.height,r="dark"===(t=["dark","light","light-outline"].includes(t)?t:"light")?"dark":"light",e=["short","long"].includes(e)?e:"long",s=i('');return s.css("height",n+"px"),s.addClass(t+" "+e),"long"===e&&function(t,e,a){t.css("background-image","url("+e+")");var n=document.createElement("img");n.onerror=function(){t.css("background-image","url("+a+")")},n.src=e}(s,"https://www.gstatic.com/instantbuy/svg/"+r+"/"+a+".svg","https://www.gstatic.com/instantbuy/svg/"+r+"/en.svg"),s},attachPaymentRequestButtonEventListeners:function(t,e){t.on("click",function(t){i("body").addClass("woocommerce-stripe-prb-clicked")}),wc_stripe_payment_request_params.is_product_page?p.attachProductPageEventListeners(t,e):p.attachCartPageEventListeners(t,e)},attachProductPageEventListeners:function(e,a){var n=[],r=i(".single_add_to_cart_button");e.on("click",function(t){return r.is(".disabled")?(t.preventDefault(),void(r.is(".wc-variation-is-unavailable")?window.alert(wc_add_to_cart_variation_params.i18n_unavailable_text):r.is(".wc-variation-selection-needed")&&window.alert(wc_add_to_cart_variation_params.i18n_make_a_selection_text))):0').addClass("stripe-source").attr("name","stripe_source").val(e.source.id)),c("form#add_payment_method").length||c("#wc-stripe-change-payment-method").length?m.sourceSetup(e):m.form.trigger("submit"))},sourceSetup:function(r){var e={error:{type:"api_connection_error"}};c.post({url:m.getAjaxURL("create_setup_intent"),dataType:"json",data:{stripe_source_id:r.source.id,nonce:wc_stripe_params.add_card_nonce},error:function(){c(document.body).trigger("stripeError",e)}}).done(function(e){if("success"===e.status)return c("form#add_payment_method").length&&c(m.form).off("submit",m.form.onSubmit),void m.form.trigger("submit");"requires_action"===e.status?n.confirmCardSetup(e.client_secret,{payment_method:r.source.id}).then(function(e){e.error?c(document.body).trigger("stripeError",e):(c("form#add_payment_method").length&&c(m.form).off("submit",m.form.onSubmit),m.form.trigger("submit"))}).catch(function(e){console.log(e),c(document.body).trigger("stripeError",{error:e})}):c(document.body).trigger("stripeError",e)})},onSubmit:function(){return!m.isStripeChosen()||(!(!m.isStripeSaveCardChosen()&&!m.hasSource())||(!!(m.isBancontactChosen()||m.isGiropayChosen()||m.isIdealChosen()||m.isAlipayChosen()||m.isSofortChosen()||m.isP24Chosen()||m.isEpsChosen()||m.isMultibancoChosen())||(m.block(),m.createSource(),!1)))},onCCFormChange:function(){m.reset()},reset:function(){c(".wc-stripe-error, .stripe-source").remove()},onSepaError:function(e){var r=m.getSelectedPaymentElement().parents("li").eq(0).find(".stripe-source-errors");e.error?(console.log(e.error.message),c(r).html('
'),c(r).find("li").text(e.error.message)):c(r).html("")},onError:function(e,r){var t,o=r.error.message,n=m.getSelectedPaymentElement().closest("li"),i=n.find(".woocommerce-SavedPaymentMethods-tokenInput");if(c("body").hasClass("woocommerce-stripe-prb-clicked"))c("body").removeClass("woocommerce-stripe-prb-clicked"),t=c("div.woocommerce-notices-wrapper").first();else if(i.length){var s=i.filter(":checked");t=s.closest(".woocommerce-SavedPaymentMethods-new").length?c("#wc-stripe-cc-form .stripe-source-errors"):s.closest("li").find(".stripe-source-errors")}else t=n.find(".stripe-source-errors");if(m.isSepaChosen()&&"invalid_owner_name"===r.error.code&&wc_stripe_params.hasOwnProperty(r.error.code)){var a=c('');return a.find("li").text(wc_stripe_params[r.error.code]),void m.submitError(a.html())}"email_invalid"===r.error.code?o=wc_stripe_params.email_invalid:"invalid_request_error"!==r.error.type&&"api_connection_error"!==r.error.type&&"api_error"!==r.error.type&&"authentication_error"!==r.error.type&&"rate_limit_error"!==r.error.type||(o=wc_stripe_params.invalid_request_error),"card_error"===r.error.type&&wc_stripe_params.hasOwnProperty(r.error.code)&&(o=wc_stripe_params[r.error.code]),"validation_error"===r.error.type&&wc_stripe_params.hasOwnProperty(r.error.code)&&(o=wc_stripe_params[r.error.code]),m.reset(),c(".woocommerce-NoticeGroup-checkout").remove(),console.log(r.error.message),c(t).html(''),c(t).find("li").text(o),c(".wc-stripe-error").length&&c("html, body").animate({scrollTop:c(".wc-stripe-error").offset().top-200},200),m.unblock(),c.unblockUI()},submitError:function(e){c(".woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message").remove(),m.form.prepend(''+e+"
"),m.form.removeClass("processing").unblock(),m.form.find(".input-text, select, input:checkbox").trigger("blur");var r="";c("#add_payment_method").length&&(r=c("#add_payment_method")),c("#order_review").length&&(r=c("#order_review")),c("form.checkout").length&&(r=c("form.checkout")),r.length&&c("html, body").animate({scrollTop:r.offset().top-100},500),c(document.body).trigger("checkout_error"),m.unblock()},onHashChange:function(){var e=window.location.hash.match(/^#?confirm-(pi|si)-([^:]+):(.+)$/);if(e&&!(e.length<4)){var r=e[1],t=e[2],o=decodeURIComponent(e[3]);window.location.hash="",m.openIntentModal(t,o,!1,"si"===r)}},maybeConfirmIntent:function(){if(c("#stripe-intent-id").length&&c("#stripe-intent-return").length){var e=c("#stripe-intent-id").val(),r=c("#stripe-intent-return").val();m.openIntentModal(e,r,!0,!1)}},openIntentModal:function(e,t,r,o){n[o?"handleCardSetup":"handleCardPayment"](e).then(function(e){if(e.error)throw e.error;var r=e[o?"setupIntent":"paymentIntent"];"requires_capture"!==r.status&&"succeeded"!==r.status||(window.location=t)}).catch(function(e){r?window.location=t:(c(document.body).trigger("stripeError",{error:e}),m.form&&m.form.removeClass("processing"),c.get(t+"&is_ajax"))})},onEarlyRenewalSubmit:function(e){return e.preventDefault(),c.ajax({url:c("#early_renewal_modal_submit").attr("href"),method:"get",success:function(e){var r=JSON.parse(e);r.stripe_sca_required?m.openIntentModal(r.intent_secret,r.redirect_url,!0,!1):window.location=r.redirect_url}}),!1}};m.init()});
+jQuery(function(a){"use strict";try{var n=Stripe(wc_stripe_params.key,{locale:wc_stripe_params.stripe_locale||"auto"})}catch(e){return void console.log(e)}var t,o,i,e=Object.keys(wc_stripe_params.elements_options).length?wc_stripe_params.elements_options:{},r=Object.keys(wc_stripe_params.sepa_elements_options).length?wc_stripe_params.sepa_elements_options:{},s=n.elements(e),c=s.create("iban",r),m={getAjaxURL:function(e){return wc_stripe_params.ajaxurl.toString().replace("%%endpoint%%","wc_stripe_"+e)},unmountElements:function(){"yes"===wc_stripe_params.inline_cc_form?t.unmount("#stripe-card-element"):(t.unmount("#stripe-card-element"),o.unmount("#stripe-exp-element"),i.unmount("#stripe-cvc-element"))},mountElements:function(){a("#stripe-card-element").length&&("yes"!==wc_stripe_params.inline_cc_form?(t.mount("#stripe-card-element"),o.mount("#stripe-exp-element"),i.mount("#stripe-cvc-element")):t.mount("#stripe-card-element"))},createElements:function(){var e={base:{iconColor:"#666EE8",color:"#31325F",fontSize:"15px","::placeholder":{color:"#CFD7E0"}}},r={focus:"focused",empty:"empty",invalid:"invalid"},e=wc_stripe_params.elements_styling||e,r=wc_stripe_params.elements_classes||r;"yes"===wc_stripe_params.inline_cc_form?(t=s.create("card",{style:e,hidePostalCode:!0})).addEventListener("change",function(e){m.onCCFormChange(),e.error&&a(document.body).trigger("stripeError",e)}):(t=s.create("cardNumber",{style:e,classes:r}),o=s.create("cardExpiry",{style:e,classes:r}),i=s.create("cardCvc",{style:e,classes:r}),t.addEventListener("change",function(e){m.onCCFormChange(),m.updateCardBrand(e.brand),e.error&&a(document.body).trigger("stripeError",e)}),o.addEventListener("change",function(e){m.onCCFormChange(),e.error&&a(document.body).trigger("stripeError",e)}),i.addEventListener("change",function(e){m.onCCFormChange(),e.error&&a(document.body).trigger("stripeError",e)})),"yes"===wc_stripe_params.is_checkout?a(document.body).on("updated_checkout",function(){a("#stripe-card-element").children().length||(t&&m.unmountElements(),m.mountElements(),a("#stripe-iban-element").length&&c.mount("#stripe-iban-element"))}):(a("form#add_payment_method").length||a("form#order_review").length)&&(m.mountElements(),a("#stripe-iban-element").length&&c.mount("#stripe-iban-element"))},updateCardBrand:function(e){var r={visa:"stripe-visa-brand",mastercard:"stripe-mastercard-brand",amex:"stripe-amex-brand",discover:"stripe-discover-brand",diners:"stripe-diners-brand",jcb:"stripe-jcb-brand",unknown:"stripe-credit-card-brand"},t=a(".stripe-card-brand"),e=e in r?r[e]:"stripe-credit-card-brand";a.each(r,function(e,r){t.removeClass(r)}),t.addClass(e)},init:function(){"yes"!==wc_stripe_params.is_change_payment_page&&"yes"!==wc_stripe_params.is_pay_for_order_page||a(document.body).trigger("wc-credit-card-form-init"),a("form.woocommerce-checkout").length&&(this.form=a("form.woocommerce-checkout")),a("form.woocommerce-checkout").on("checkout_place_order_stripe checkout_place_order_stripe_bancontact checkout_place_order_stripe_sofort checkout_place_order_stripe_giropay checkout_place_order_stripe_ideal checkout_place_order_stripe_alipay checkout_place_order_stripe_sepa",this.onSubmit),a("form#order_review").length&&(this.form=a("form#order_review")),a("form#order_review, form#add_payment_method").on("submit",this.onSubmit),a("form#add_payment_method").length&&(this.form=a("form#add_payment_method")),a("form.woocommerce-checkout").on("change",this.reset),a(document).on("stripeError",this.onError).on("checkout_error",this.reset),c.on("change",this.onSepaError),a("#early_renewal_modal_submit").on("click",this.onEarlyRenewalSubmit),m.createElements(),window.addEventListener("hashchange",m.onHashChange),m.maybeConfirmIntent()},isStripeChosen:function(){return a("#payment_method_stripe, #payment_method_stripe_bancontact, #payment_method_stripe_sofort, #payment_method_stripe_giropay, #payment_method_stripe_ideal, #payment_method_stripe_alipay, #payment_method_stripe_sepa, #payment_method_stripe_eps, #payment_method_stripe_multibanco").is(":checked")||a("#payment_method_stripe").is(":checked")&&"new"===a('input[name="wc-stripe-payment-token"]:checked').val()||a("#payment_method_stripe_sepa").is(":checked")&&"new"===a('input[name="wc-stripe-payment-token"]:checked').val()},isStripeSaveCardChosen:function(){return a("#payment_method_stripe").is(":checked")&&a('input[name="wc-stripe-payment-token"]').is(":checked")&&"new"!==a('input[name="wc-stripe-payment-token"]:checked').val()||a("#payment_method_stripe_sepa").is(":checked")&&a('input[name="wc-stripe_sepa-payment-token"]').is(":checked")&&"new"!==a('input[name="wc-stripe_sepa-payment-token"]:checked').val()},isStripeCardChosen:function(){return a("#payment_method_stripe").is(":checked")},isBancontactChosen:function(){return a("#payment_method_stripe_bancontact").is(":checked")},isGiropayChosen:function(){return a("#payment_method_stripe_giropay").is(":checked")},isIdealChosen:function(){return a("#payment_method_stripe_ideal").is(":checked")},isSofortChosen:function(){return a("#payment_method_stripe_sofort").is(":checked")},isAlipayChosen:function(){return a("#payment_method_stripe_alipay").is(":checked")},isSepaChosen:function(){return a("#payment_method_stripe_sepa").is(":checked")},isP24Chosen:function(){return a("#payment_method_stripe_p24").is(":checked")},isEpsChosen:function(){return a("#payment_method_stripe_eps").is(":checked")},isMultibancoChosen:function(){return a("#payment_method_stripe_multibanco").is(":checked")},hasSource:function(){return 0').addClass("stripe-source").attr("name","stripe_source").val(e.source.id)),a("form#add_payment_method").length||a("#wc-stripe-change-payment-method").length?m.sourceSetup(e):m.form.trigger("submit"))},sourceSetup:function(r){var e={error:{type:"api_connection_error"}};a.post({url:m.getAjaxURL("create_setup_intent"),dataType:"json",data:{stripe_source_id:r.source.id,nonce:wc_stripe_params.add_card_nonce},error:function(){a(document.body).trigger("stripeError",e)}}).done(function(e){return"success"===e.status?(a("form#add_payment_method").length&&a(m.form).off("submit",m.form.onSubmit),void m.form.trigger("submit")):void("requires_action"===e.status?n.confirmCardSetup(e.client_secret,{payment_method:r.source.id}).then(function(e){e.error?a(document.body).trigger("stripeError",e):(a("form#add_payment_method").length&&a(m.form).off("submit",m.form.onSubmit),m.form.trigger("submit"))}).catch(function(e){console.log(e),a(document.body).trigger("stripeError",{error:e})}):a(document.body).trigger("stripeError",e))})},onSubmit:function(){return!m.isStripeChosen()||(!(!m.isStripeSaveCardChosen()&&!m.hasSource())||(!!(m.isBancontactChosen()||m.isGiropayChosen()||m.isIdealChosen()||m.isAlipayChosen()||m.isSofortChosen()||m.isP24Chosen()||m.isEpsChosen()||m.isMultibancoChosen())||(m.block(),m.createSource(),!1)))},onCCFormChange:function(){m.reset()},reset:function(){a(".wc-stripe-error, .stripe-source").remove()},onSepaError:function(e){var r=m.getSelectedPaymentElement().parents("li").eq(0).find(".stripe-source-errors");e.error?(console.log(e.error.message),a(r).html(''),a(r).find("li").text(e.error.message)):a(r).html("")},onError:function(e,r){var t,o=r.error.message,n=m.getSelectedPaymentElement().closest("li"),i=n.find(".woocommerce-SavedPaymentMethods-tokenInput"),s=a("body").hasClass("woocommerce-stripe-prb-clicked");if(t=s?(a("body").removeClass("woocommerce-stripe-prb-clicked"),a("div.woocommerce-notices-wrapper").first()):i.length?(t=i.filter(":checked")).closest(".woocommerce-SavedPaymentMethods-new").length?a("#wc-stripe-cc-form .stripe-source-errors"):t.closest("li").find(".stripe-source-errors"):n.find(".stripe-source-errors"),m.isSepaChosen()&&"invalid_owner_name"===r.error.code&&wc_stripe_params.hasOwnProperty(r.error.code)){n=a('');return n.find("li").text(wc_stripe_params[r.error.code]),void m.submitError(n.html())}"email_invalid"===r.error.code?o=wc_stripe_params.email_invalid:"invalid_request_error"!==r.error.type&&"api_connection_error"!==r.error.type&&"api_error"!==r.error.type&&"authentication_error"!==r.error.type&&"rate_limit_error"!==r.error.type||(o=wc_stripe_params.invalid_request_error),"card_error"===r.error.type&&wc_stripe_params.hasOwnProperty(r.error.code)&&(o=wc_stripe_params[r.error.code]),"validation_error"===r.error.type&&wc_stripe_params.hasOwnProperty(r.error.code)&&(o=wc_stripe_params[r.error.code]),m.reset(),a(".woocommerce-NoticeGroup-checkout").remove(),console.log(r.error.message),a(t).html(''),a(t).find("li").text(o),a(".wc-stripe-error").length&&a("html, body").animate({scrollTop:a(".wc-stripe-error").offset().top-200},200),m.unblock(),a.unblockUI()},submitError:function(e){a(".woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message").remove(),m.form.prepend(''+e+"
"),m.form.removeClass("processing").unblock(),m.form.find(".input-text, select, input:checkbox").trigger("blur");e="";a("#add_payment_method").length&&(e=a("#add_payment_method")),a("#order_review").length&&(e=a("#order_review")),(e=a("form.checkout").length?a("form.checkout"):e).length&&a("html, body").animate({scrollTop:e.offset().top-100},500),a(document.body).trigger("checkout_error"),m.unblock()},onHashChange:function(){var e,r,t=window.location.hash.match(/^#?confirm-(pi|si)-([^:]+):(.+)$/);!t||t.length<4||(e=t[1],r=t[2],t=decodeURIComponent(t[3]),window.location.hash="",m.openIntentModal(r,t,!1,"si"===e))},maybeConfirmIntent:function(){var e,r;a("#stripe-intent-id").length&&a("#stripe-intent-return").length&&(e=a("#stripe-intent-id").val(),r=a("#stripe-intent-return").val(),m.openIntentModal(e,r,!0,!1))},openIntentModal:function(e,r,t,o){n[o?"handleCardSetup":"handleCardPayment"](e).then(function(e){if(e.error)throw e.error;e=e[o?"setupIntent":"paymentIntent"];"requires_capture"!==e.status&&"succeeded"!==e.status||(window.location=r)}).catch(function(e){t?window.location=r:(a(document.body).trigger("stripeError",{error:e}),m.form&&m.form.removeClass("processing"),a.get(r+"&is_ajax"))})},onEarlyRenewalSubmit:function(e){return e.preventDefault(),a.ajax({url:a("#early_renewal_modal_submit").attr("href"),method:"get",success:function(e){e=JSON.parse(e);e.stripe_sca_required?m.openIntentModal(e.intent_secret,e.redirect_url,!0,!1):window.location=e.redirect_url}}),!1}};m.init()});
diff --git a/changelog.txt b/changelog.txt
index 13587690c..8d2def61a 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -5,6 +5,8 @@
* Fix - Orders won't transition to 'Refunded' state if refund can't be created.
* Fix - Normalize United Kingdom and Canada postal codes for Apple Pay.
* Fix - Subscription sign-up fees not included in total for Payment Request Button.
+* Add - Support for Credit Card payments (incl. 3DS payments) via WooCommerce Blocks; limited to WooCommerce Core product types.
+* Add - Support for payments (incl. 3DS payments) paid via Payment Request Buttons in WooCommerce Blocks; limited to WooCommerce Core product types.
= 5.1.0 - 2021-04-07 =
* Fix - Don't attempt to submit level 3 data for non-US merchants.
diff --git a/client/api/index.js b/client/api/index.js
new file mode 100644
index 000000000..7beed6c52
--- /dev/null
+++ b/client/api/index.js
@@ -0,0 +1,75 @@
+/* global wc_stripe_payment_request_params */
+
+/**
+ * External dependencies
+ */
+import $ from 'jquery';
+
+/**
+ * Internal dependencies
+ */
+import { normalizeOrderData, normalizeAddress } from '../blocks/stripe-utils';
+
+const getAjaxUrl = ( endpoint ) => {
+ return wc_stripe_payment_request_params.ajax_url
+ .toString()
+ .replace( '%%endpoint%%', 'wc_stripe_' + endpoint );
+};
+
+export const getCartDetails = () => {
+ const data = {
+ security: wc_stripe_payment_request_params.nonce.payment,
+ };
+
+ return $.ajax( {
+ type: 'POST',
+ data,
+ url: getAjaxUrl( 'get_cart_details' ),
+ } );
+};
+
+/**
+ * Update shipping options.
+ *
+ * @param {Object} address Customer address.
+ * @param {string} paymentRequestType Either 'apple_pay' or 'payment_request_api' depending on the type of request.
+ */
+export const updateShippingOptions = ( address, paymentRequestType ) => {
+ const data = {
+ security: wc_stripe_payment_request_params.nonce.shipping,
+ payment_request_type: paymentRequestType,
+ is_product_page: wc_stripe_payment_request_params.is_product_page,
+ ...normalizeAddress( address ),
+ };
+
+ return $.ajax( {
+ type: 'POST',
+ data,
+ url: getAjaxUrl( 'get_shipping_options' ),
+ } );
+};
+
+export const updateShippingDetails = ( shippingOption ) => {
+ const data = {
+ security: wc_stripe_payment_request_params.nonce.update_shipping,
+ shipping_method: [ shippingOption.id ],
+ is_product_page: wc_stripe_payment_request_params.is_product_page,
+ };
+
+ return $.ajax( {
+ type: 'POST',
+ data,
+ url: getAjaxUrl( 'update_shipping_method' ),
+ } );
+};
+
+export const createOrder = ( sourceEvent, paymentRequestType ) => {
+ const data = normalizeOrderData( sourceEvent, paymentRequestType );
+
+ return $.ajax( {
+ type: 'POST',
+ data,
+ dataType: 'json',
+ url: getAjaxUrl( 'create_order' ),
+ } );
+};
diff --git a/client/blocks/credit-card/constants.js b/client/blocks/credit-card/constants.js
new file mode 100644
index 000000000..24e1e1d47
--- /dev/null
+++ b/client/blocks/credit-card/constants.js
@@ -0,0 +1 @@
+export const PAYMENT_METHOD_NAME = 'stripe';
diff --git a/client/blocks/credit-card/elements.js b/client/blocks/credit-card/elements.js
new file mode 100644
index 000000000..fb04977e2
--- /dev/null
+++ b/client/blocks/credit-card/elements.js
@@ -0,0 +1,161 @@
+/**
+ * External dependencies
+ */
+import { useState } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import {
+ CardElement,
+ CardNumberElement,
+ CardExpiryElement,
+ CardCvcElement,
+} from '@stripe/react-stripe-js';
+
+/**
+ * Internal dependencies
+ */
+import { useElementOptions } from './use-element-options';
+
+/** @typedef {import('react')} React */
+
+const baseTextInputStyles = 'wc-block-gateway-input';
+
+/**
+ * InlineCard component
+ *
+ * @param {Object} props Incoming props for the component.
+ * @param {React.ReactElement} props.inputErrorComponent
+ * @param {function(any):any} props.onChange
+ */
+export const InlineCard = ( {
+ inputErrorComponent: ValidationInputError,
+ onChange,
+} ) => {
+ const [ isEmpty, setIsEmpty ] = useState( true );
+ const { options, onActive, error, setError } = useElementOptions( {
+ hidePostalCode: true,
+ } );
+ const errorCallback = ( event ) => {
+ if ( event.error ) {
+ setError( event.error.message );
+ } else {
+ setError( '' );
+ }
+ setIsEmpty( event.empty );
+ onChange( event );
+ };
+ return (
+ <>
+
+ onActive( isEmpty ) }
+ onFocus={ () => onActive( isEmpty ) }
+ onChange={ errorCallback }
+ />
+
+
+
+ >
+ );
+};
+
+/**
+ * CardElements component.
+ *
+ * @param {Object} props
+ * @param {function(any):any} props.onChange
+ * @param {React.ReactElement} props.inputErrorComponent
+ */
+export const CardElements = ( {
+ onChange,
+ inputErrorComponent: ValidationInputError,
+} ) => {
+ const [ isEmpty, setIsEmpty ] = useState( {
+ cardNumber: true,
+ cardExpiry: true,
+ cardCvc: true,
+ } );
+ const {
+ options: cardNumOptions,
+ onActive: cardNumOnActive,
+ error: cardNumError,
+ setError: cardNumSetError,
+ } = useElementOptions( { showIcon: false } );
+ const {
+ options: cardExpiryOptions,
+ onActive: cardExpiryOnActive,
+ error: cardExpiryError,
+ setError: cardExpirySetError,
+ } = useElementOptions();
+ const {
+ options: cardCvcOptions,
+ onActive: cardCvcOnActive,
+ error: cardCvcError,
+ setError: cardCvcSetError,
+ } = useElementOptions();
+ const errorCallback = ( errorSetter, elementId ) => ( event ) => {
+ if ( event.error ) {
+ errorSetter( event.error.message );
+ } else {
+ errorSetter( '' );
+ }
+ setIsEmpty( { ...isEmpty, [ elementId ]: event.empty } );
+ onChange( event );
+ };
+ return (
+
+
+ cardNumOnActive( isEmpty.cardNumber ) }
+ onBlur={ () => cardNumOnActive( isEmpty.cardNumber ) }
+ />
+
+
+
+
+ cardExpiryOnActive( isEmpty.cardExpiry ) }
+ onBlur={ () => cardExpiryOnActive( isEmpty.cardExpiry ) }
+ id="wc-stripe-card-expiry-element"
+ />
+
+
+
+
+ cardCvcOnActive( isEmpty.cardCvc ) }
+ onBlur={ () => cardCvcOnActive( isEmpty.cardCvc ) }
+ id="wc-stripe-card-code-element"
+ />
+
+
+
+
+ );
+};
diff --git a/client/blocks/credit-card/index.js b/client/blocks/credit-card/index.js
new file mode 100644
index 000000000..6b3267fad
--- /dev/null
+++ b/client/blocks/credit-card/index.js
@@ -0,0 +1,70 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useEffect, useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { getStripeServerData, loadStripe } from '../stripe-utils';
+import { StripeCreditCard, getStripeCreditCardIcons } from './payment-method';
+import { ThreeDSecurePaymentHandler } from '../three-d-secure';
+import { PAYMENT_METHOD_NAME } from './constants';
+
+const stripePromise = loadStripe();
+
+const StripeComponent = ( { RenderedComponent, ...props } ) => {
+ const [ errorMessage, setErrorMessage ] = useState( '' );
+
+ useEffect( () => {
+ Promise.resolve( stripePromise ).then( ( { error } ) => {
+ if ( error ) {
+ setErrorMessage( error.message );
+ }
+ } );
+ }, [ setErrorMessage ] );
+
+ useEffect( () => {
+ if ( errorMessage ) {
+ throw new Error( errorMessage );
+ }
+ }, [ errorMessage ] );
+
+ return ;
+};
+
+const StripeLabel = ( props ) => {
+ const { PaymentMethodLabel } = props.components;
+
+ const labelText = getStripeServerData().title
+ ? getStripeServerData().title
+ : __( 'Credit / Debit Card', 'woocommerce-gateway-stripe' );
+
+ return ;
+};
+
+const cardIcons = getStripeCreditCardIcons();
+const stripeCcPaymentMethod = {
+ name: PAYMENT_METHOD_NAME,
+ label: ,
+ content: ,
+ edit: ,
+ savedTokenComponent: (
+
+ ),
+ icons: cardIcons,
+ canMakePayment: () => stripePromise,
+ ariaLabel: __(
+ 'Stripe Credit Card payment method',
+ 'woocommerce-gateway-stripe'
+ ),
+ supports: {
+ // Use `false` as fallback values in case server provided configuration is missing.
+ showSavedCards: getStripeServerData().showSavedCards ?? false,
+ showSaveOption: getStripeServerData().showSaveOption ?? false,
+ features: getStripeServerData()?.supports ?? [],
+ },
+};
+
+export default stripeCcPaymentMethod;
diff --git a/client/blocks/credit-card/payment-method.js b/client/blocks/credit-card/payment-method.js
new file mode 100644
index 000000000..49a7d0b1a
--- /dev/null
+++ b/client/blocks/credit-card/payment-method.js
@@ -0,0 +1,91 @@
+/**
+ * External dependencies
+ */
+import { Elements, useStripe } from '@stripe/react-stripe-js';
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { getStripeServerData } from '../stripe-utils';
+import { useCheckoutSubscriptions } from './use-checkout-subscriptions';
+import { InlineCard, CardElements } from './elements';
+
+/**
+ * @typedef {import('../stripe-utils/type-defs').Stripe} Stripe
+ * @typedef {import('../stripe-utils/type-defs').StripePaymentRequest} StripePaymentRequest
+ * @typedef {import('@woocommerce/type-defs/registered-payment-method-props').RegisteredPaymentMethodProps} RegisteredPaymentMethodProps
+ */
+
+export const getStripeCreditCardIcons = () => {
+ return Object.entries( getStripeServerData().icons ).map(
+ ( [ id, { src, alt } ] ) => {
+ return {
+ id,
+ src,
+ alt,
+ };
+ }
+ );
+};
+
+/**
+ * Stripe Credit Card component
+ *
+ * @param {RegisteredPaymentMethodProps} props Incoming props
+ */
+const CreditCardComponent = ( {
+ billing,
+ eventRegistration,
+ emitResponse,
+ components,
+} ) => {
+ const { ValidationInputError, PaymentMethodIcons } = components;
+ const [ sourceId, setSourceId ] = useState( '' );
+ const stripe = useStripe();
+ const onStripeError = useCheckoutSubscriptions(
+ eventRegistration,
+ billing,
+ sourceId,
+ setSourceId,
+ emitResponse,
+ stripe
+ );
+ const onChange = ( paymentEvent ) => {
+ if ( paymentEvent.error ) {
+ onStripeError( paymentEvent );
+ }
+ setSourceId( '' );
+ };
+ const cardIcons = getStripeCreditCardIcons();
+
+ const renderedCardElement = getStripeServerData().inline_cc_form ? (
+
+ ) : (
+
+ );
+ return (
+ <>
+ { renderedCardElement }
+ { PaymentMethodIcons && cardIcons.length && (
+
+ ) }
+ >
+ );
+};
+
+export const StripeCreditCard = ( props ) => {
+ const { stripe } = props;
+
+ return (
+
+
+
+ );
+};
diff --git a/client/blocks/credit-card/use-checkout-subscriptions.js b/client/blocks/credit-card/use-checkout-subscriptions.js
new file mode 100644
index 000000000..acfd057ce
--- /dev/null
+++ b/client/blocks/credit-card/use-checkout-subscriptions.js
@@ -0,0 +1,97 @@
+/**
+ * External dependencies
+ */
+import { useEffect, useCallback, useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { getErrorMessageForTypeAndCode } from '../stripe-utils';
+import { usePaymentIntents } from '../three-d-secure';
+import { usePaymentProcessing } from './use-payment-processing';
+
+/**
+ * @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EventRegistrationProps} EventRegistrationProps
+ * @typedef {import('@woocommerce/type-defs/registered-payment-method-props').BillingDataProps} BillingDataProps
+ * @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EmitResponseProps} EmitResponseProps
+ * @typedef {import('../stripe-utils/type-defs').Stripe} Stripe
+ * @typedef {import('react').Dispatch} SourceIdDispatch
+ */
+
+/**
+ * A custom hook for the Stripe processing and event observer logic.
+ *
+ * @param {EventRegistrationProps} eventRegistration Event registration functions.
+ * @param {BillingDataProps} billing Various billing data items.
+ * @param {string} sourceId Current set stripe source id.
+ * @param {SourceIdDispatch} setSourceId Setter for stripe source id.
+ * @param {EmitResponseProps} emitResponse Various helpers for usage with observer
+ * response objects.
+ * @param {Stripe} stripe The stripe.js object.
+ *
+ * @return {function(Object):Object} Returns a function for handling stripe error.
+ */
+export const useCheckoutSubscriptions = (
+ eventRegistration,
+ billing,
+ sourceId,
+ setSourceId,
+ emitResponse,
+ stripe
+) => {
+ const [ error, setError ] = useState( '' );
+ const onStripeError = useCallback( ( event ) => {
+ const type = event.error.type;
+ const code = event.error.code || '';
+ const message =
+ getErrorMessageForTypeAndCode( type, code ) ?? event.error.message;
+ setError( message );
+ return message;
+ }, [] );
+ const {
+ onCheckoutAfterProcessingWithSuccess,
+ onPaymentProcessing,
+ onCheckoutAfterProcessingWithError,
+ } = eventRegistration;
+ usePaymentIntents(
+ stripe,
+ onCheckoutAfterProcessingWithSuccess,
+ setSourceId,
+ emitResponse
+ );
+ usePaymentProcessing(
+ onStripeError,
+ error,
+ stripe,
+ billing,
+ emitResponse,
+ sourceId,
+ setSourceId,
+ onPaymentProcessing
+ );
+ // hook into and register callbacks for events.
+ useEffect( () => {
+ const onError = ( { processingResponse } ) => {
+ if ( processingResponse?.paymentDetails?.errorMessage ) {
+ return {
+ type: emitResponse.responseTypes.ERROR,
+ message: processingResponse.paymentDetails.errorMessage,
+ messageContext: emitResponse.noticeContexts.PAYMENTS,
+ };
+ }
+ // so we don't break the observers.
+ return true;
+ };
+ const unsubscribeAfterProcessing = onCheckoutAfterProcessingWithError(
+ onError
+ );
+ return () => {
+ unsubscribeAfterProcessing();
+ };
+ }, [
+ onCheckoutAfterProcessingWithError,
+ emitResponse.noticeContexts.PAYMENTS,
+ emitResponse.responseTypes.ERROR,
+ ] );
+ return onStripeError;
+};
diff --git a/client/blocks/credit-card/use-element-options.js b/client/blocks/credit-card/use-element-options.js
new file mode 100644
index 000000000..17243c9a8
--- /dev/null
+++ b/client/blocks/credit-card/use-element-options.js
@@ -0,0 +1,115 @@
+/**
+ * External dependencies
+ */
+import { useState, useEffect, useCallback } from '@wordpress/element';
+
+/**
+ * @typedef {import('../stripe-utils/type-defs').StripeElementOptions} StripeElementOptions
+ */
+
+/**
+ * Returns the value of a specific CSS property for the element matched by the provided selector.
+ *
+ * @param {string} selector CSS selector that matches the element to query.
+ * @param {string} property Name of the property to retrieve the style
+ * value from.
+ * @param {string} defaultValue Fallback value if the value for the property
+ * could not be retrieved.
+ *
+ * @return {string} The style value of that property in the document element.
+ */
+const getComputedStyle = ( selector, property, defaultValue ) => {
+ let elementStyle = {};
+
+ if (
+ typeof document === 'object' &&
+ typeof document.querySelector === 'function' &&
+ typeof window.getComputedStyle === 'function'
+ ) {
+ const element = document.querySelector( selector );
+ if ( element ) {
+ elementStyle = window.getComputedStyle( element );
+ }
+ }
+
+ return elementStyle[ property ] || defaultValue;
+};
+
+/**
+ * Default options for the stripe elements.
+ */
+const elementOptions = {
+ style: {
+ base: {
+ iconColor: '#666EE8',
+ color: '#31325F',
+ fontSize: getComputedStyle(
+ '.wc-block-checkout',
+ 'fontSize',
+ '16px'
+ ),
+ lineHeight: 1.375, // With a font-size of 16px, line-height will be 22px.
+ '::placeholder': {
+ color: '#fff',
+ },
+ },
+ },
+ classes: {
+ focus: 'focused',
+ empty: 'empty',
+ invalid: 'has-error',
+ },
+};
+
+/**
+ * A custom hook handling options implemented on the stripe elements.
+ *
+ * @param {Object} [overloadedOptions] An array of extra options to merge with
+ * the options provided for the element.
+ *
+ * @return {StripeElementOptions} The stripe element options interface
+ */
+export const useElementOptions = ( overloadedOptions ) => {
+ const [ isActive, setIsActive ] = useState( false );
+ const [ options, setOptions ] = useState( {
+ ...elementOptions,
+ ...overloadedOptions,
+ } );
+ const [ error, setError ] = useState( '' );
+
+ useEffect( () => {
+ const color = isActive ? '#CFD7E0' : '#fff';
+
+ setOptions( ( prevOptions ) => {
+ const showIcon =
+ typeof prevOptions.showIcon !== 'undefined'
+ ? { showIcon: isActive }
+ : {};
+ return {
+ ...prevOptions,
+ style: {
+ ...prevOptions.style,
+ base: {
+ ...prevOptions.style.base,
+ '::placeholder': {
+ color,
+ },
+ },
+ },
+ ...showIcon,
+ };
+ } );
+ }, [ isActive ] );
+
+ const onActive = useCallback(
+ ( isEmpty ) => {
+ if ( ! isEmpty ) {
+ setIsActive( true );
+ } else {
+ setIsActive( ( prevActive ) => ! prevActive );
+ }
+ },
+ [ setIsActive ]
+ );
+ return { options, onActive, error, setError };
+};
diff --git a/client/blocks/credit-card/use-payment-processing.js b/client/blocks/credit-card/use-payment-processing.js
new file mode 100644
index 000000000..29b2abe2b
--- /dev/null
+++ b/client/blocks/credit-card/use-payment-processing.js
@@ -0,0 +1,166 @@
+/**
+ * External dependencies
+ */
+import { useEffect } from '@wordpress/element';
+import {
+ CardElement,
+ CardNumberElement,
+ useElements,
+} from '@stripe/react-stripe-js';
+
+/**
+ * Internal dependencies
+ */
+import { PAYMENT_METHOD_NAME } from './constants';
+import {
+ getStripeServerData,
+ getErrorMessageForTypeAndCode,
+} from '../stripe-utils';
+import { errorTypes } from '../stripe-utils/constants';
+
+/**
+ * @typedef {import('@stripe/stripe-js').Stripe} Stripe
+ * @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EventRegistrationProps} EventRegistrationProps
+ * @typedef {import('@woocommerce/type-defs/registered-payment-method-props').BillingDataProps} BillingDataProps
+ * @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EmitResponseProps} EmitResponseProps
+ * @typedef {import('react').Dispatch} SourceIdDispatch
+ */
+
+/**
+ * @typedef {function(function():any):function():void} EventRegistration
+ */
+
+/**
+ * A custom hook that registers stripe payment processing with the
+ * onPaymentProcessing event from checkout.
+ *
+ * @param {function(any):string} onStripeError Sets an error for stripe.
+ * @param {string} error Any set error message (an empty string if no
+ * error).
+ * @param {Stripe} stripe The stripe utility
+ * @param {BillingDataProps} billing Various billing data items.
+ * @param {EmitResponseProps} emitResponse Various helpers for usage with observer
+ * response objects.
+ * @param {string} sourceId Current set stripe source id.
+ * @param {SourceIdDispatch} setSourceId Setter for stripe source id.
+ * @param {EventRegistration} onPaymentProcessing The event emitter for processing payment.
+ */
+export const usePaymentProcessing = (
+ onStripeError,
+ error,
+ stripe,
+ billing,
+ emitResponse,
+ sourceId,
+ setSourceId,
+ onPaymentProcessing
+) => {
+ const elements = useElements();
+ // hook into and register callbacks for events
+ useEffect( () => {
+ const createSource = async ( ownerInfo ) => {
+ const elementToGet = getStripeServerData().inline_cc_form
+ ? CardElement
+ : CardNumberElement;
+ return await stripe.createSource(
+ // @ts-ignore
+ elements?.getElement( elementToGet ),
+ {
+ type: 'card',
+ owner: ownerInfo,
+ }
+ );
+ };
+ const onSubmit = async () => {
+ try {
+ const billingData = billing.billingData;
+ // if there's an error return that.
+ if ( error ) {
+ return {
+ type: emitResponse.responseTypes.ERROR,
+ message: error,
+ };
+ }
+ // use token if it's set.
+ if ( sourceId !== '' ) {
+ return {
+ type: emitResponse.responseTypes.SUCCESS,
+ meta: {
+ paymentMethodData: {
+ paymentMethod: PAYMENT_METHOD_NAME,
+ paymentRequestType: 'cc',
+ stripe_source: sourceId,
+ },
+ billingData,
+ },
+ };
+ }
+ const ownerInfo = {
+ address: {
+ line1: billingData.address_1,
+ line2: billingData.address_2,
+ city: billingData.city,
+ state: billingData.state,
+ postal_code: billingData.postcode,
+ country: billingData.country,
+ },
+ };
+ if ( billingData.phone ) {
+ ownerInfo.phone = billingData.phone;
+ }
+ if ( billingData.email ) {
+ ownerInfo.email = billingData.email;
+ }
+ if ( billingData.first_name || billingData.last_name ) {
+ ownerInfo.name = `${ billingData.first_name } ${ billingData.last_name }`;
+ }
+
+ const response = await createSource( ownerInfo );
+ if ( response.error ) {
+ return {
+ type: emitResponse.responseTypes.ERROR,
+ message: onStripeError( response ),
+ };
+ }
+ if ( ! response.source || ! response.source.id ) {
+ throw new Error(
+ getErrorMessageForTypeAndCode( errorTypes.API_ERROR )
+ );
+ }
+ setSourceId( response.source.id );
+ return {
+ type: emitResponse.responseTypes.SUCCESS,
+ meta: {
+ paymentMethodData: {
+ stripe_source: response.source.id,
+ paymentMethod: PAYMENT_METHOD_NAME,
+ paymentRequestType: 'cc',
+ },
+ billingData,
+ },
+ };
+ } catch ( e ) {
+ return {
+ type: emitResponse.responseTypes.ERROR,
+ message: e,
+ };
+ }
+ };
+ const unsubscribeProcessing = onPaymentProcessing( onSubmit );
+ return () => {
+ unsubscribeProcessing();
+ };
+ }, [
+ onPaymentProcessing,
+ billing.billingData,
+ stripe,
+ sourceId,
+ setSourceId,
+ onStripeError,
+ error,
+ emitResponse.noticeContexts.PAYMENTS,
+ emitResponse.responseTypes.ERROR,
+ emitResponse.responseTypes.SUCCESS,
+ elements,
+ ] );
+};
diff --git a/client/blocks/index.js b/client/blocks/index.js
new file mode 100644
index 000000000..bdd30b87e
--- /dev/null
+++ b/client/blocks/index.js
@@ -0,0 +1,19 @@
+/**
+ * External dependencies
+ */
+import {
+ registerPaymentMethod,
+ registerExpressPaymentMethod,
+} from '@woocommerce/blocks-registry';
+
+/**
+ * Internal dependencies
+ */
+import stripeCcPaymentMethod from './credit-card';
+import paymentRequestPaymentMethod from './payment-request';
+
+// Register Stripe Credit Card.
+registerPaymentMethod( stripeCcPaymentMethod );
+
+// Register Stripe Payment Request.
+registerExpressPaymentMethod( paymentRequestPaymentMethod );
diff --git a/client/blocks/payment-request/apple-pay-preview.js b/client/blocks/payment-request/apple-pay-preview.js
new file mode 100644
index 000000000..cc740802c
--- /dev/null
+++ b/client/blocks/payment-request/apple-pay-preview.js
@@ -0,0 +1,2 @@
+export const applePayImage =
+ "data:image/svg+xml,%3Csvg width='264' height='48' viewBox='0 0 264 48' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='264' height='48' rx='3' fill='black'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M125.114 16.6407C125.682 15.93 126.067 14.9756 125.966 14C125.135 14.0415 124.121 14.549 123.533 15.2602C123.006 15.8693 122.539 16.8641 122.661 17.7983C123.594 17.8797 124.526 17.3317 125.114 16.6407Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M125.955 17.982C124.601 17.9011 123.448 18.7518 122.801 18.7518C122.154 18.7518 121.163 18.0224 120.092 18.0421C118.696 18.0629 117.402 18.8524 116.694 20.1079C115.238 22.6196 116.31 26.3453 117.726 28.3909C118.414 29.4028 119.242 30.5174 120.334 30.4769C121.366 30.4365 121.77 29.8087 123.024 29.8087C124.277 29.8087 124.641 30.4769 125.733 30.4567C126.865 30.4365 127.573 29.4443 128.261 28.4313C129.049 27.2779 129.373 26.1639 129.393 26.1027C129.373 26.0825 127.209 25.2515 127.189 22.7606C127.169 20.6751 128.888 19.6834 128.969 19.6217C127.998 18.1847 126.481 18.0224 125.955 17.982Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M136.131 23.1804H138.834C140.886 23.1804 142.053 22.0752 142.053 20.1592C142.053 18.2432 140.886 17.1478 138.845 17.1478H136.131V23.1804ZM139.466 15.1582C142.411 15.1582 144.461 17.1903 144.461 20.1483C144.461 23.1172 142.369 25.1596 139.392 25.1596H136.131V30.3498H133.775V15.1582H139.466Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M152.198 26.224V25.3712L149.579 25.5397C148.106 25.6341 147.339 26.182 147.339 27.14C147.339 28.0664 148.138 28.6667 149.39 28.6667C150.988 28.6667 152.198 27.6449 152.198 26.224ZM145.046 27.2032C145.046 25.2551 146.529 24.1395 149.263 23.971L152.198 23.7922V22.9498C152.198 21.7181 151.388 21.0442 149.947 21.0442C148.758 21.0442 147.896 21.6548 147.717 22.5916H145.592C145.656 20.6232 147.507 19.1914 150.01 19.1914C152.703 19.1914 154.459 20.602 154.459 22.7917V30.351H152.282V28.5298H152.229C151.609 29.719 150.241 30.4666 148.758 30.4666C146.571 30.4666 145.046 29.1612 145.046 27.2032Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M156.461 34.4145V32.5934C156.608 32.6141 156.965 32.6354 157.155 32.6354C158.196 32.6354 158.785 32.1932 159.142 31.0564L159.353 30.3824L155.366 19.3281H157.827L160.604 28.298H160.657L163.434 19.3281H165.832L161.698 30.9402C160.752 33.6038 159.668 34.4778 157.376 34.4778C157.197 34.4778 156.618 34.4565 156.461 34.4145Z' fill='white'/%3E%3C/svg%3E%0A";
diff --git a/client/blocks/payment-request/branded-buttons.js b/client/blocks/payment-request/branded-buttons.js
new file mode 100644
index 000000000..9bfea6a40
--- /dev/null
+++ b/client/blocks/payment-request/branded-buttons.js
@@ -0,0 +1,85 @@
+/**
+ * External dependencies
+ */
+import { useState, useEffect } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { getStripeServerData } from '../stripe-utils';
+
+export const shouldUseGooglePayBrand = () => {
+ const ua = window.navigator.userAgent.toLowerCase();
+ const isChrome =
+ /chrome/.test( ua ) &&
+ ! /edge|edg|opr|brave\//.test( ua ) &&
+ window.navigator.vendor === 'Google Inc.';
+ // newer versions of Brave do not have the userAgent string
+ const isBrave = isChrome && window.navigator.brave;
+ return isChrome && ! isBrave;
+};
+
+const useLocalizedGoogleSvg = ( type, theme, locale ) => {
+ // If we're using the short button type (i.e. logo only) make sure we get the logo only SVG.
+ const googlePlaySvg =
+ type === 'long'
+ ? `https://www.gstatic.com/instantbuy/svg/${ theme }/${ locale }.svg`
+ : `https://www.gstatic.com/instantbuy/svg/${ theme }_gpay.svg`;
+
+ const [ url, setUrl ] = useState( googlePlaySvg );
+
+ useEffect( () => {
+ const im = document.createElement( 'img' );
+ im.addEventListener( 'error', () => {
+ setUrl(
+ `https://www.gstatic.com/instantbuy/svg/${ theme }/en.svg`
+ );
+ } );
+ im.src = url;
+ }, [ url, theme ] );
+
+ return url;
+};
+
+export const GooglePayButton = ( { onButtonClicked } ) => {
+ const {
+ theme = 'dark',
+ locale = 'en',
+ height = '44',
+ } = getStripeServerData()?.button;
+
+ const allowedTypes = [ 'short', 'long' ];
+ // Use pre-blocks settings until we merge the two distinct settings objects.
+ /* global wc_stripe_payment_request_params */
+ const { branded_type } = wc_stripe_payment_request_params.button; // eslint-disable-line camelcase
+ const type = allowedTypes.includes( branded_type ) ? branded_type : 'long'; // eslint-disable-line camelcase
+
+ // Allowed themes for Google Pay button image are 'dark' and 'light'.
+ // We may include 'light-outline' as a theme, so we ensure only 'dark' or 'light' are possible
+ // here.
+ const gpayButtonTheme = theme === 'dark' ? 'dark' : 'light';
+
+ // Let's make sure the localized Google Pay button exists, otherwise we fall back to the
+ // english version. This test element is not used on purpose.
+ const backgroundUrl = useLocalizedGoogleSvg(
+ type,
+ gpayButtonTheme,
+ locale
+ );
+
+ return (
+
+ );
+};
diff --git a/client/blocks/payment-request/constants.js b/client/blocks/payment-request/constants.js
new file mode 100644
index 000000000..c7007d3bd
--- /dev/null
+++ b/client/blocks/payment-request/constants.js
@@ -0,0 +1 @@
+export const PAYMENT_METHOD_NAME = 'payment_request';
diff --git a/client/blocks/payment-request/custom-button.js b/client/blocks/payment-request/custom-button.js
new file mode 100644
index 000000000..94626132a
--- /dev/null
+++ b/client/blocks/payment-request/custom-button.js
@@ -0,0 +1,30 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { getStripeServerData } from '../stripe-utils';
+
+export const CustomButton = ( { onButtonClicked } ) => {
+ const {
+ theme = 'dark',
+ height = '44',
+ customLabel = __( 'Buy now', 'woocommerce-gateway-stripe' ),
+ } = getStripeServerData().button;
+ return (
+
+ );
+};
diff --git a/client/blocks/payment-request/event-handlers.js b/client/blocks/payment-request/event-handlers.js
new file mode 100644
index 000000000..ae93bda04
--- /dev/null
+++ b/client/blocks/payment-request/event-handlers.js
@@ -0,0 +1,219 @@
+/* global wc_stripe_payment_request_params */
+
+/**
+ * External dependencies
+ */
+import $ from 'jquery';
+
+/**
+ * Internal dependencies
+ */
+import {
+ updateShippingOptions,
+ updateShippingDetails,
+ createOrder,
+} from '../../api';
+
+const shippingAddressChangeHandler = ( paymentRequestType ) => ( evt ) => {
+ const { shippingAddress } = evt;
+
+ // Update the payment request shipping information address.
+ updateShippingOptions( shippingAddress, paymentRequestType ).then(
+ ( response ) => {
+ evt.updateWith( {
+ status: response.result,
+ shippingOptions: response.shipping_options,
+ total: response.total,
+ displayItems: response.displayItems,
+ } );
+ }
+ );
+};
+
+const shippingOptionChangeHandler = ( evt ) => {
+ const { shippingOption } = evt;
+
+ // Update the shipping rates for the order.
+ updateShippingDetails( shippingOption ).then( ( response ) => {
+ if ( response.result === 'success' ) {
+ evt.updateWith( {
+ status: 'success',
+ total: response.total,
+ displayItems: response.displayItems,
+ } );
+ }
+
+ if ( response.result === 'fail' ) {
+ evt.updateWith( { status: 'fail' } );
+ }
+ } );
+};
+
+/**
+ * Helper function. Returns payment intent information from the provided URL.
+ * If no information is embedded in the URL this function returns `undefined`.
+ *
+ * @param {string} url - The url to check for partials.
+ *
+ * @return {Object|undefined} The object containing `type`, `clientSecret`, and
+ * `redirectUrl`. Undefined if no partails embedded in the url.
+ */
+const getRedirectUrlPartials = ( url ) => {
+ const partials = url.match( /^#?confirm-(pi|si)-([^:]+):(.+)$/ );
+
+ if ( ! partials || partials.length < 4 ) {
+ return undefined;
+ }
+
+ const type = partials[ 1 ];
+ const clientSecret = partials[ 2 ];
+ const redirectUrl = decodeURIComponent( partials[ 3 ] );
+
+ return {
+ type,
+ clientSecret,
+ redirectUrl,
+ };
+};
+
+/**
+ * Helper function. Requests that the provided intent (identified by the secret) is be
+ * handled by Stripe. Returns a promise from Stripe.
+ *
+ * @param {Object} stripe - The stripe object.
+ * @param {string} intentType - The type of intent. Either `pi` or `si`.
+ * @param {string} clientSecret - Client secret returned from Stripe.
+ *
+ * @return {Promise} A promise from Stripe with the confirmed intent or an error.
+ */
+const requestIntentConfirmation = ( stripe, intentType, clientSecret ) => {
+ const isSetupIntent = intentType === 'si';
+
+ if ( isSetupIntent ) {
+ return stripe.handleCardSetup( clientSecret );
+ }
+ return stripe.handleCardPayment( clientSecret );
+};
+
+/**
+ * Helper function. Returns the payment or setup intent from a given confirmed intent.
+ *
+ * @param {Object} intent - The confirmed intent.
+ * @param {string} intentType - The payment intent's type. Either `pi` or `si`.
+ *
+ * @return {Object} The Stripe payment or setup intent.
+ */
+const getIntentFromConfirmation = ( intent, intentType ) => {
+ const isSetupIntent = intentType === 'si';
+
+ if ( isSetupIntent ) {
+ return intent.setupIntent;
+ }
+ return intent.paymentIntent;
+};
+
+const doesIntentRequireCapture = ( intent ) => {
+ return intent.status === 'requires_capture';
+};
+
+const didIntentSucceed = ( intent ) => {
+ return intent.status === 'succeeded';
+};
+
+/**
+ * Helper function; part of a promise chain.
+ * Receives a possibly confirmed payment intent from Stripe and proceeds to charge the
+ * payment method of the intent was confirmed successfully.
+ *
+ * @param {string} redirectUrl - The URL to redirect to after a successful payment.
+ * @param {string} intentType - The type of the payment intent. Either `pi` or `si`.
+ */
+const handleIntentConfirmation = ( redirectUrl, intentType ) => (
+ confirmation
+) => {
+ if ( confirmation.error ) {
+ throw confirmation.error;
+ }
+
+ const intent = getIntentFromConfirmation( confirmation, intentType );
+ if ( doesIntentRequireCapture( intent ) || didIntentSucceed( intent ) ) {
+ // If the 3DS verification was successful we can proceed with checkout as usual.
+ window.location = redirectUrl;
+ }
+};
+
+/**
+ * Helper function; part of a promise chain.
+ * Receives the response from our server after we attempt to create an order through
+ * our AJAX API, proceeds with payment if possible, otherwise attempts to confirm the
+ * payment (i.e. 3DS verification) through Stripe.
+ *
+ * @param {Object} stripe - The Stripe JS object.
+ * @param {Object} evt - The `source` event from the Stripe payment request button.
+ * @param {Function} setExpressPaymentError - Used to show error messages to the customer.
+ */
+const performPayment = ( stripe, evt, setExpressPaymentError ) => (
+ createOrderResponse
+) => {
+ if ( createOrderResponse.result === 'success' ) {
+ evt.complete( 'success' );
+
+ const partials = getRedirectUrlPartials( createOrderResponse.redirect );
+
+ // If no information is embedded in the URL that means the payment doesn't need
+ // verification and we can proceed as usual.
+ if ( ! partials || partials.length < 4 ) {
+ window.location = createOrderResponse.redirect;
+ return;
+ }
+
+ const { type, clientSecret, redirectUrl } = partials;
+
+ // The payment requires 3DS verification, so we try to take care of that here.
+ requestIntentConfirmation( stripe, type, clientSecret )
+ .then( handleIntentConfirmation( redirectUrl, type ) )
+ .catch( ( error ) => {
+ setExpressPaymentError( error.message );
+
+ // Report back to the server.
+ $.get( redirectUrl + '&is_ajax' );
+ } );
+ } else {
+ evt.complete( 'fail' );
+
+ // WooCommerce returns a messege embedded in a notice via HTML here, so we need
+ // to extract the actual message from the notice.
+ const div = document.createElement( 'div' );
+ div.innerHTML = createOrderResponse.messages;
+ const errorMessage = div?.firstChild?.textContent ?? '';
+
+ setExpressPaymentError( errorMessage );
+ }
+};
+
+const paymentProcessingHandler = (
+ stripe,
+ paymentRequestType,
+ setExpressPaymentError
+) => ( evt ) => {
+ const allowPrepaidCards =
+ wc_stripe_payment_request_params?.stripe?.allow_prepaid_card === 'yes';
+
+ // Check if we allow prepaid cards.
+ if ( ! allowPrepaidCards && evt?.source?.card?.funding === 'prepaid' ) {
+ setExpressPaymentError(
+ wc_stripe_payment_request_params?.i18n?.no_prepaid_card
+ );
+ } else {
+ // Create the order and attempt to pay.
+ createOrder( evt, paymentRequestType ).then(
+ performPayment( stripe, evt, setExpressPaymentError )
+ );
+ }
+};
+
+export {
+ shippingAddressChangeHandler,
+ shippingOptionChangeHandler,
+ paymentProcessingHandler,
+};
diff --git a/client/blocks/payment-request/hooks.js b/client/blocks/payment-request/hooks.js
new file mode 100644
index 000000000..fc1f89f6e
--- /dev/null
+++ b/client/blocks/payment-request/hooks.js
@@ -0,0 +1,184 @@
+/**
+ * External dependencies
+ */
+import { useState, useEffect, useCallback } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { getCartDetails } from '../../api';
+import {
+ shippingAddressChangeHandler,
+ shippingOptionChangeHandler,
+ paymentProcessingHandler,
+} from './event-handlers';
+import { createPaymentRequestUsingCart } from '../stripe-utils';
+
+/**
+ * This hook takes care of creating a payment request and making sure
+ * you can pay through said payment request.
+ *
+ * @param {Object} stripe The stripe object used to create the payment request.
+ * @param {boolean} needsShipping A value from the Block checkout that indicates whether shipping
+ * is required or not.
+ * @param {Object} billing - The billing data from the checkout or cart block.
+ *
+ * @return {Array} An array; first element is the payment request; second element is the payment
+ * requests type.
+ */
+export const usePaymentRequest = ( stripe, needsShipping, billing ) => {
+ const [ paymentRequest, setPaymentRequest ] = useState( null );
+ const [ paymentRequestType, setPaymentRequestType ] = useState( null );
+
+ // Create a payment request if:
+ // a) Stripe object is loaded; and
+ // b) There is no payment request created already.
+ useEffect( () => {
+ // Do nothing if Stripe object isn't loaded or paymentRequest already exists.
+ if ( ! stripe || paymentRequest ) {
+ return;
+ }
+
+ getCartDetails().then( ( cart ) => {
+ const pr = createPaymentRequestUsingCart( stripe, cart );
+
+ pr.canMakePayment().then( ( result ) => {
+ if ( result ) {
+ setPaymentRequest( pr );
+ if ( result.applePay ) {
+ setPaymentRequestType( 'apple_pay' );
+ } else if ( result.googlePay ) {
+ setPaymentRequestType( 'google_pay' );
+ } else {
+ setPaymentRequestType( 'payment_request_api' );
+ }
+ }
+ } );
+ } );
+ }, [ paymentRequest, stripe ] );
+
+ // Reset the payment request if the need for shipping changes.
+ useEffect( () => {
+ setPaymentRequest( null );
+ }, [
+ needsShipping,
+ billing.cartTotal,
+ billing.cartTotalItems,
+ billing.currency.code,
+ ] );
+
+ return [ paymentRequest, paymentRequestType ];
+};
+
+/**
+ * Adds a shipping address change event handler to the provided payment request. Updates the
+ * order's shipping address when necessary.
+ *
+ * @param {Object} paymentRequest - The payment request object.
+ * @param {string} paymentRequestType - The payment request type.
+ */
+export const useShippingAddressUpdateHandler = (
+ paymentRequest,
+ paymentRequestType
+) => {
+ useEffect( () => {
+ // Need to use `?.` here in case paymentRequest is null.
+ const shippingAddressUpdateHandler = paymentRequest?.on(
+ 'shippingaddresschange',
+ shippingAddressChangeHandler( paymentRequestType )
+ );
+
+ return () => {
+ // Need to use `?.` here in case shippingAddressUpdateHandler is null.
+ shippingAddressUpdateHandler?.removeAllListeners();
+ };
+ }, [ paymentRequest, paymentRequestType ] );
+};
+
+/**
+ * Adds a shipping option change event handler to the provided payment request.
+ *
+ * @param {Object} paymentRequest - The payment request object.
+ * @param {string} paymentRequestType - The payment request type.
+ */
+export const useShippingOptionChangeHandler = (
+ paymentRequest,
+ paymentRequestType
+) => {
+ useEffect( () => {
+ // Need to use `?.` here in case paymentRequest is null.
+ const sippingOptionHandler = paymentRequest?.on(
+ 'shippingoptionchange',
+ shippingOptionChangeHandler
+ );
+
+ return () => {
+ // Need to use `?.` here in case shippingAddressHandler is null.
+ sippingOptionHandler?.removeAllListeners();
+ };
+ }, [ paymentRequest, paymentRequestType ] );
+};
+
+/**
+ * Adds a payment event handler to the provided payment request.
+ *
+ * @param {Object} stripe - The stripe object used to confirm and create a payment intent.
+ * @param {Object} paymentRequest - The payment request object.
+ * @param {string} paymentRequestType - The payment request type.
+ * @param {Function} setExpressPaymentError - A function used to expose an error message to show
+ * the customer.
+ */
+export const useProcessPaymentHandler = (
+ stripe,
+ paymentRequest,
+ paymentRequestType,
+ setExpressPaymentError
+) => {
+ useEffect( () => {
+ const paymentMethodUpdateHandler = paymentRequest?.on(
+ 'source',
+ paymentProcessingHandler(
+ stripe,
+ paymentRequestType,
+ setExpressPaymentError
+ )
+ );
+
+ return () => {
+ paymentMethodUpdateHandler?.removeAllListeners();
+ };
+ }, [ stripe, paymentRequest, paymentRequestType, setExpressPaymentError ] );
+};
+
+/**
+ * Returns an onClick handler for payment request buttons. Resets the error state, syncs the
+ * payment request with the block, and calls the provided click handler.
+ *
+ * @param {Function} setExpressPaymentError - Used to set the error state.
+ * @param {Function} onClick - The onClick function that should be called on click.
+ *
+ * @return {Function} An onClick handler for the payment request buttons.
+ */
+export const useOnClickHandler = ( setExpressPaymentError, onClick ) => {
+ return useCallback( () => {
+ // Reset any Payment Request errors.
+ setExpressPaymentError( '' );
+
+ // Call the Blocks API `onClick` handler.
+ onClick();
+ }, [ setExpressPaymentError, onClick ] );
+};
+
+/**
+ * Adds a cancellation handler to the provided payment request.
+ *
+ * @param {Object} paymentRequest - The payment request object.
+ * @param {Function} onClose - A function from the Blocks API.
+ */
+export const useCancelHandler = ( paymentRequest, onClose ) => {
+ useEffect( () => {
+ paymentRequest?.on( 'cancel', () => {
+ onClose();
+ } );
+ }, [ paymentRequest, onClose ] );
+};
diff --git a/client/blocks/payment-request/index.js b/client/blocks/payment-request/index.js
new file mode 100644
index 000000000..1a876b7a7
--- /dev/null
+++ b/client/blocks/payment-request/index.js
@@ -0,0 +1,58 @@
+/* global wc_stripe_payment_request_params */
+
+/**
+ * External dependencies
+ */
+import { getSetting } from '@woocommerce/settings';
+
+/**
+ * Internal dependencies
+ */
+import { PAYMENT_METHOD_NAME } from './constants';
+import { PaymentRequestExpress } from './payment-request-express';
+import { applePayImage } from './apple-pay-preview';
+import { getStripeServerData, loadStripe } from '../stripe-utils';
+
+const ApplePayPreview = () => ;
+
+const componentStripePromise = loadStripe();
+
+const paymentRequestPaymentMethod = {
+ name: PAYMENT_METHOD_NAME,
+ content: ,
+ edit: ,
+ canMakePayment: ( cartData ) => {
+ // If the `wc_stripe_payment_request_params` object is not available we don't support
+ // payment requests.
+ // eslint-disable-next-line camelcase
+ if ( typeof wc_stripe_payment_request_params === 'undefined' ) {
+ return false;
+ }
+
+ return loadStripe().then( ( stripe ) => {
+ // Create a payment request and check if we can make a payment to determine whether to
+ // show the Payment Request Button or not. This is necessary because a browser might be
+ // able to load the Stripe JS object, but not support Payment Requests.
+ const paymentRequest = stripe.paymentRequest( {
+ total: {
+ label: 'Total',
+ amount: parseInt(
+ cartData?.cartTotals?.total_price ?? 0,
+ 10
+ ),
+ pending: true,
+ },
+ country: getSetting( 'baseLocation', {} )?.country,
+ currency: cartData?.cartTotals?.currency_code?.toLowerCase(),
+ } );
+
+ return paymentRequest.canMakePayment();
+ } );
+ },
+ paymentMethodId: 'stripe',
+ supports: {
+ features: getStripeServerData()?.supports ?? [],
+ },
+};
+
+export default paymentRequestPaymentMethod;
diff --git a/client/blocks/payment-request/payment-request-express.js b/client/blocks/payment-request/payment-request-express.js
new file mode 100644
index 000000000..3956232a5
--- /dev/null
+++ b/client/blocks/payment-request/payment-request-express.js
@@ -0,0 +1,153 @@
+/**
+ * External dependencies
+ */
+import {
+ Elements,
+ PaymentRequestButtonElement,
+ useStripe,
+} from '@stripe/react-stripe-js';
+
+/**
+ * Internal dependencies
+ */
+import { getStripeServerData } from '../stripe-utils';
+import { GooglePayButton, shouldUseGooglePayBrand } from './branded-buttons';
+import { CustomButton } from './custom-button';
+import {
+ usePaymentRequest,
+ useProcessPaymentHandler,
+ useShippingAddressUpdateHandler,
+ useShippingOptionChangeHandler,
+ useOnClickHandler,
+ useCancelHandler,
+} from './hooks';
+
+/**
+ * @typedef {import('../stripe-utils/type-defs').Stripe} Stripe
+ * @typedef {import('../stripe-utils/type-defs').StripePaymentRequest} StripePaymentRequest
+ * @typedef {import('@woocommerce/type-defs/registered-payment-method-props').RegisteredPaymentMethodProps} RegisteredPaymentMethodProps
+ */
+
+/**
+ * @typedef {Object} WithStripe
+ *
+ * @property {Stripe} [stripe] Stripe api (might not be present)
+ */
+
+/**
+ * @typedef {RegisteredPaymentMethodProps & WithStripe} StripeRegisteredPaymentMethodProps
+ */
+
+/**
+ * PaymentRequestExpressComponent
+ *
+ * @param {StripeRegisteredPaymentMethodProps} props Incoming props
+ */
+const PaymentRequestExpressComponent = ( {
+ billing,
+ shippingData,
+ onClick,
+ onClose,
+ setExpressPaymentError,
+} ) => {
+ const stripe = useStripe();
+ const { needsShipping } = shippingData;
+
+ /* Set up payment request and its event handlers. */
+ const [ paymentRequest, paymentRequestType ] = usePaymentRequest(
+ stripe,
+ needsShipping,
+ billing
+ );
+ useShippingAddressUpdateHandler( paymentRequest, paymentRequestType );
+ useShippingOptionChangeHandler( paymentRequest, paymentRequestType );
+ useProcessPaymentHandler(
+ stripe,
+ paymentRequest,
+ paymentRequestType,
+ setExpressPaymentError
+ );
+ const onPaymentRequestButtonClick = useOnClickHandler(
+ setExpressPaymentError,
+ onClick
+ );
+ useCancelHandler( paymentRequest, onClose );
+
+ // locale is not a valid value for the paymentRequestButton style.
+ // Make sure `theme` defaults to 'dark' if it's not found in the server provided configuration.
+ const {
+ type = 'default',
+ theme = 'dark',
+ height = '48',
+ } = getStripeServerData().button;
+
+ const paymentRequestButtonStyle = {
+ paymentRequestButton: {
+ type,
+ theme,
+ height: `${ height }px`,
+ },
+ };
+
+ // Use pre-blocks settings until we merge the two distinct settings objects.
+ /* global wc_stripe_payment_request_params */
+ const isBranded = wc_stripe_payment_request_params.button.is_branded;
+ const brandedType = wc_stripe_payment_request_params.button.branded_type;
+ const isCustom = wc_stripe_payment_request_params.button.is_custom;
+
+ if ( ! paymentRequest ) {
+ return null;
+ }
+
+ // Prepare the onClick handler for our custom made Payment Request buttons.
+ const customAndBrandedClickHandler = () => {
+ onPaymentRequestButtonClick();
+ paymentRequest.show();
+ };
+
+ if ( isCustom ) {
+ return (
+
+ );
+ }
+
+ if ( isBranded && shouldUseGooglePayBrand() ) {
+ return (
+
+ );
+ }
+
+ if ( isBranded ) {
+ // Not implemented branded buttons default to Stripe's button.
+ // Apple Pay buttons can also fall back to Stripe's button, as it's already branded.
+ // Set button type to default or buy, depending on branded type, to avoid issues with Stripe.
+ paymentRequestButtonStyle.paymentRequestButton.type =
+ brandedType === 'long' ? 'buy' : 'default';
+ }
+
+ return (
+
+ );
+};
+
+/**
+ * PaymentRequestExpress with stripe provider
+ *
+ * @param {StripeRegisteredPaymentMethodProps} props
+ */
+export const PaymentRequestExpress = ( props ) => {
+ const { stripe } = props;
+ return (
+
+
+
+ );
+};
diff --git a/client/blocks/stripe-utils/constants.js b/client/blocks/stripe-utils/constants.js
new file mode 100644
index 000000000..7257f91cc
--- /dev/null
+++ b/client/blocks/stripe-utils/constants.js
@@ -0,0 +1,28 @@
+export const errorTypes = {
+ INVALID_EMAIL: 'email_invalid',
+ INVALID_REQUEST: 'invalid_request_error',
+ API_CONNECTION: 'api_connection_error',
+ API_ERROR: 'api_error',
+ AUTHENTICATION_ERROR: 'authentication_error',
+ RATE_LIMIT_ERROR: 'rate_limit_error',
+ CARD_ERROR: 'card_error',
+ VALIDATION_ERROR: 'validation_error',
+};
+
+export const errorCodes = {
+ INVALID_NUMBER: 'invalid_number',
+ INVALID_EXPIRY_MONTH: 'invalid_expiry_month',
+ INVALID_EXPIRY_YEAR: 'invalid_expiry_year',
+ INVALID_CVC: 'invalid_cvc',
+ INCORRECT_NUMBER: 'incorrect_number',
+ INCOMPLETE_NUMBER: 'incomplete_number',
+ INCOMPLETE_CVC: 'incomplete_cvc',
+ INCOMPLETE_EXPIRY: 'incomplete_expiry',
+ EXPIRED_CARD: 'expired_card',
+ INCORRECT_CVC: 'incorrect_cvc',
+ INCORRECT_ZIP: 'incorrect_zip',
+ INVALID_EXPIRY_YEAR_PAST: 'invalid_expiry_year_past',
+ CARD_DECLINED: 'card_declined',
+ MISSING: 'missing',
+ PROCESSING_ERROR: 'processing_error',
+};
diff --git a/client/blocks/stripe-utils/index.js b/client/blocks/stripe-utils/index.js
new file mode 100644
index 000000000..6bbd44578
--- /dev/null
+++ b/client/blocks/stripe-utils/index.js
@@ -0,0 +1,3 @@
+export * from './normalize';
+export * from './utils';
+export * from './load-stripe';
diff --git a/client/blocks/stripe-utils/load-stripe.js b/client/blocks/stripe-utils/load-stripe.js
new file mode 100644
index 000000000..7c2479c12
--- /dev/null
+++ b/client/blocks/stripe-utils/load-stripe.js
@@ -0,0 +1,28 @@
+/* global wc_stripe_params */
+
+/**
+ * External dependencies
+ */
+import { loadStripe } from '@stripe/stripe-js';
+
+/**
+ * Internal dependencies
+ */
+import { getApiKey } from './utils';
+
+const stripePromise = () =>
+ new Promise( ( resolve ) => {
+ try {
+ // Default to the 'auto' locale so Stripe chooses the browser's locale
+ // if the store's locale is not available.
+ // eslint-disable-next-line camelcase
+ const { stripe_locale: locale = 'auto' } = wc_stripe_params;
+ resolve( loadStripe( getApiKey(), { locale } ) );
+ } catch ( error ) {
+ // In order to avoid showing console error publicly to users,
+ // we resolve instead of rejecting when there is an error.
+ resolve( { error } );
+ }
+ } );
+
+export { stripePromise as loadStripe };
diff --git a/client/blocks/stripe-utils/normalize.js b/client/blocks/stripe-utils/normalize.js
new file mode 100644
index 000000000..2df3bd299
--- /dev/null
+++ b/client/blocks/stripe-utils/normalize.js
@@ -0,0 +1,105 @@
+/* global wc_stripe_payment_request_params */
+
+/**
+ * @typedef {import('./type-defs').StripePaymentItem} StripePaymentItem
+ * @typedef {import('./type-defs').StripeShippingOption} StripeShippingOption
+ * @typedef {import('./type-defs').StripeShippingAddress} StripeShippingAddress
+ * @typedef {import('./type-defs').StripePaymentResponse} StripePaymentResponse
+ * @typedef {import('@woocommerce/type-defs/registered-payment-method-props').PreparedCartTotalItem} CartTotalItem
+ * @typedef {import('@woocommerce/type-defs/cart').CartShippingOption} CartShippingOption
+ * @typedef {import('@woocommerce/type-defs/shipping').ShippingAddress} CartShippingAddress
+ * @typedef {import('@woocommerce/type-defs/billing').BillingData} CartBillingAddress
+ */
+
+/**
+ * Normalizes order data received upon creating an order using the store's AJAX API.
+ *
+ * @param {Object} sourceEvent - The source event that triggered the creation of the order.
+ * @param {string} paymentRequestType - The payment request type.
+ */
+const normalizeOrderData = ( sourceEvent, paymentRequestType ) => {
+ const { source } = sourceEvent;
+ const email = source?.owner?.email;
+ const phone = source?.owner?.phone;
+ const billing = source?.owner?.address;
+ const name = source?.owner?.name;
+ const shipping = sourceEvent?.shippingAddress;
+
+ const data = {
+ _wpnonce: wc_stripe_payment_request_params.nonce.checkout,
+ billing_first_name:
+ name?.split( ' ' )?.slice( 0, 1 )?.join( ' ' ) ?? '',
+ billing_last_name: name?.split( ' ' )?.slice( 1 )?.join( ' ' ) ?? '',
+ billing_company: '',
+ billing_email: email ?? sourceEvent?.payerEmail,
+ billing_phone:
+ phone ?? sourceEvent?.payerPhone?.replace( '/[() -]/g', '' ),
+ billing_country: billing?.country ?? '',
+ billing_address_1: billing?.line1 ?? '',
+ billing_address_2: billing?.line2 ?? '',
+ billing_city: billing?.city ?? '',
+ billing_state: billing?.state ?? '',
+ billing_postcode: billing?.postal_code ?? '',
+ shipping_first_name: '',
+ shipping_last_name: '',
+ shipping_company: '',
+ shipping_country: '',
+ shipping_address_1: '',
+ shipping_address_2: '',
+ shipping_city: '',
+ shipping_state: '',
+ shipping_postcode: '',
+ shipping_method: [ sourceEvent?.shippingOption?.id ],
+ order_comments: '',
+ payment_method: 'stripe',
+ ship_to_different_address: 1,
+ terms: 1,
+ stripe_source: source.id,
+ payment_request_type: paymentRequestType,
+ };
+
+ if ( shipping ) {
+ data.shipping_first_name = shipping?.recipient
+ ?.split( ' ' )
+ ?.slice( 0, 1 )
+ ?.join( ' ' );
+ data.shipping_last_name = shipping?.recipient
+ ?.split( ' ' )
+ ?.slice( 1 )
+ ?.join( ' ' );
+ data.shipping_company = shipping?.organization;
+ data.shipping_country = shipping?.country;
+ data.shipping_address_1 = shipping?.addressLine?.[ 0 ] ?? '';
+ data.shipping_address_2 = shipping?.addressLine?.[ 1 ] ?? '';
+ data.shipping_city = shipping?.city;
+ data.shipping_state = shipping?.region;
+ data.shipping_postcode = shipping?.postalCode;
+ }
+
+ return data;
+};
+
+/**
+ * Normalizes an address received upon updating shipping options using the store's AJAX API.
+ *
+ * @param {Object} address - The address that needs to be normalized.
+ * @return {Object} The normalized address.
+ */
+const normalizeAddress = ( address ) => {
+ return {
+ country: address.country,
+ state: address.region,
+ postcode: address.postalCode,
+ city: address.city,
+ address:
+ typeof address.addressLine[ 0 ] === 'undefined'
+ ? ''
+ : address.addressLine[ 0 ],
+ address_2:
+ typeof address.addressLine[ 1 ] === 'undefined'
+ ? ''
+ : address.addressLine[ 1 ],
+ };
+};
+
+export { normalizeOrderData, normalizeAddress };
diff --git a/client/blocks/stripe-utils/type-defs.js b/client/blocks/stripe-utils/type-defs.js
new file mode 100644
index 000000000..8c3981f2c
--- /dev/null
+++ b/client/blocks/stripe-utils/type-defs.js
@@ -0,0 +1,322 @@
+/**
+ * Stripe PaymentItem object
+ *
+ * @typedef {Object} StripePaymentItem
+ *
+ * @property {string} label The label for the payment item.
+ * @property {number} amount The amount for the payment item (in subunits)
+ * @property {boolean} [pending] Whether or not the amount is pending update on
+ * recalculation.
+ */
+
+/**
+ * Stripe ShippingOption object
+ *
+ * @typedef {Object} StripeShippingOption
+ *
+ * @property {string} id A unique ID for the shipping option.
+ * @property {string} label A short label for the shipping option.
+ * @property {string} detail A longer description for the shipping option.
+ * @property {number} amount The amount to show for the shipping option
+ * (in subunits)
+ */
+
+/**
+ * @typedef {Object} StripeShippingAddress
+ *
+ * @property {string} [country] Two letter country code, capitalized
+ * (ISO3166 alpha-2).
+ * @property {Array} [addressLine] An array of address line items.
+ * @property {string} [region] The most coarse subdivision of a
+ * country. (state etc)
+ * @property {string} [city] The name of a city, town, village etc.
+ * @property {string} [postalCode] The postal or ZIP code.
+ * @property {string} [recipient] The name of the recipient.
+ * @property {string} [phone] The phone number of the recipient.
+ * @property {string} [sortingCode] The sorting code as used in France.
+ * Not present on Apple platforms.
+ * @property {string} [dependentLocality] A logical subdivision of a city.
+ * Not present on Apple platforms.
+ */
+
+/**
+ * @typedef {Object} StripeBillingDetails
+ *
+ * @property {Object} [address] The billing address
+ * @property {string} [address.city] The billing address city
+ * @property {string} [address.country] The billing address country
+ * @property {string} [address.line1] The first line for the address
+ * @property {string} [address.line2] The second line fro the address
+ * @property {string} [address.postal_code] The postal/zip code
+ * @property {string} [address.state] The state
+ * @property {string} [email] The billing email
+ * @property {string} [name] The billing name
+ * @property {string} [phone] The billing phone
+ * @property {Object} [verified_address] The verified address of the owner.
+ * @property {string} [verified_email] Provided by the payment provider.
+ * @property {string} [verified_phone] Provided by the payment provider.
+ * @property {string} [verified_name] Provided by the payment provider.
+ */
+
+/**
+ * @typedef {Object} StripeBillingCard
+ *
+ * @property {string} brand The card brand
+ * @property {Object} checks Various security checks
+ * @property {string} checks.address_line1_check If an address line1 was
+ * provided, results of the
+ * check.
+ * @property {string} checks.address_postal_code_check If a postal code was
+ * provided, results of the
+ * check.
+ * @property {string} checks.cvc_check If CVC provided, results
+ * of the check.
+ * @property {string} country Two-letter ISO code for
+ * the country on the card.
+ * @property {number} exp_month Two-digit number for
+ * card expiry month.
+ * @property {number} exp_year Two-digit number for
+ * card expiry year.
+ * @property {string} fingerprint Uniquely identifies this
+ * particular card number
+ * @property {string} funding The card funding type
+ * @property {Object} generated_from Details of the original
+ * PaymentMethod that
+ * created this object.
+ * @property {string} last4 The last 4 digits of the
+ * card
+ * @property {Object} three_d_secure_usage Contains details on how
+ * this card may be used for
+ * 3d secure
+ * @property {Object} wallet If this card is part of a
+ * card wallet, this
+ * contains the details of
+ * the card wallet.
+ */
+
+/**
+ * @typedef {Object} StripePaymentMethod
+ *
+ * @property {string} id Unique identifier for the
+ * object
+ * @property {StripeBillingDetails} billing_details The billing details for the
+ * payment method
+ * @property {StripeBillingCard} card Details on the card used to
+ * pay
+ * @property {string} customer The ID of the customer to
+ * which this payment method
+ * is saved.
+ * @property {Object} metadata Set of key-value pairs that
+ * can be attached to the
+ * object.
+ * @property {string} type Type of payment method
+ * @property {string} object The type of object. Always
+ * 'payment_method'. Can use
+ * to validate!
+ * @property {Object} card_present If this is a card present
+ * payment method, contains
+ * details about that card
+ * @property {number} created The timestamp for when the
+ * card was created.
+ * @property {Object} fpx If this is an fpx payment
+ * method, contains details
+ * about it.
+ * @property {Object} ideal If this is an ideal payment
+ * method, contains details
+ * about it.
+ * @property {boolean} livemode True if the object exists
+ * in live mode or if in test
+ * mode.
+ * @property {Object} sepa_debit If this is a sepa_debit
+ * payment method, contains
+ * details about it.
+ */
+
+/**
+ * @typedef {Object} StripeSource
+ *
+ * @property {string} id Unique identifier for
+ * object
+ * @property {number} amount A positive number in
+ * the smallest currency
+ * unit.
+ * @property {string} currency The three-letter ISO
+ * code for the currency
+ * @property {string} customer The ID of the customer
+ * to which this source
+ * is attached.
+ * @property {Object} metadata Arbitrary key-value
+ * pairs that can be
+ * attached.
+ * @property {StripeBillingDetails} [owner] Information about the
+ * owner of the payment
+ * made.
+ * @property {Object} [redirect] Information related to
+ * the redirect flow
+ * (present if the source
+ * is authenticated by
+ * redirect)
+ * @property {string} statement_descriptor Extra information
+ * about a source (will
+ * appear on customer's
+ * statement)
+ * @property {string} status The status of the
+ * source.
+ * @property {string} type The type of the source
+ * (it is a payment
+ * method type)
+ * @property {string} object Value is "source" can
+ * be used to validate.
+ * @property {string} client_secret The client secret of
+ * the source. Used for
+ * client-side retrieval
+ * using a publishable
+ * key.
+ * @property {Object} [code_verification] Information related to
+ * the code verification
+ * flow.
+ * @property {number} created When the source object
+ * was instantiated
+ * (timestamp).
+ * @property {string} flow The authentication
+ * flow of the source.
+ * @property {boolean} livemode If true then payment
+ * is made in live mode
+ * otherwise test mode.
+ * @property {Object} [receiver] Information related to
+ * the receiver flow.
+ * @property {Object} source_order Information about the
+ * items and shipping
+ * associated with the
+ * source.
+ * @property {string} usage Whether source should
+ * be reusable or not.
+ */
+
+/**
+ * @typedef {Object} StripePaymentResponse
+ *
+ * @property {Object} [token] A stripe token object
+ * @property {StripePaymentMethod} [paymentMethod] The stripe payment method
+ * object
+ * @property {?StripeSource} [source] Present if this was the
+ * result of a source event
+ * listener
+ * @property {Function} complete Call this when the token
+ * data has been processed.
+ * @property {string} [payerName] The customer's name.
+ * @property {string} [payerEmail] The customer's email.
+ * @property {string} [payerPhone] The customer's phone.
+ * @property {StripeShippingAddress} [shippingAddress] The final shipping
+ * address the customer
+ * indicated
+ * @property {StripeShippingOption} [shippingOption] The final shipping
+ * option the customer
+ * selected.
+ * @property {string} methodName The unique name of the
+ * payment handler the
+ * customer chose to
+ * authorize payment
+ */
+
+/**
+ * @typedef {Object} StripePaymentRequestOptions The configuration of stripe
+ * payment request options to
+ * pass in.
+ *
+ * @property {string} country Two-letter (ISO)
+ * country code.
+ * @property {string} currency Three letter currency
+ * code.
+ * @property {StripePaymentItem} total Shown to the customer.
+ * @property {StripePaymentItem[]} displayItems Line items shown to the
+ * customer.
+ * @property {boolean} requestPayerName Whether or not to
+ * collect the payer's
+ * name.
+ * @property {boolean} requestPayerEmail Whether or not to
+ * collect the payer's
+ * email.
+ * @property {boolean} requestPayerPhone Whether or not to
+ * collect the payer's
+ * phone.
+ * @property {boolean} requestShipping Whether to collect
+ * shipping address.
+ * @property {StripeShippingOption[]} shippingOptions Available shipping
+ * options.
+ */
+
+/**
+ * @typedef {Object} StripePaymentRequest Stripe payment request object.
+ *
+ * @property {function():Promise} canMakePayment Returns a promise that resolves
+ * with an object detailing if a
+ * browser payment API is
+ * available.
+ * @property {function()} show Shows the browser's payment
+ * interface (called automatically
+ * if payment request button in
+ * use)
+ * @property {function()} update Used to update a PaymentRequest
+ * object.
+ * @property {function()} on For registering callbacks on
+ * payment request events.
+ */
+
+/**
+ * @typedef {Object} Stripe Stripe api object.
+ * @property {any} api Various api properties
+ */
+
+/**
+ * @typedef {Object} CreditCardIcon
+ *
+ * @property {string} url Url to icon.
+ * @property {string} alt Alt text for icon.
+ */
+
+/* eslint-disable jsdoc/valid-types */
+// [k:string]:CreditCardIcon triggers the above rule even though VSCode interprets it fine.
+/**
+ * @typedef {Object} StripeServerData
+ *
+ * @property {string} stripeTotalLabel The string used for payment
+ * descriptor.
+ * @property {string} publicKey The public api key for stripe
+ * requests.
+ * @property {boolean} allowPrepaidCard True means that prepaid cards
+ * can be used for payment.
+ * @property {Object} button Contains button styles
+ * @property {string} button.type The type of button.
+ * @property {string} button.theme The theme for the button.
+ * @property {string} button.height The height (in pixels) for
+ * the button.
+ * @property {string} button.locale The locale to use for stripe
+ * elements.
+ * @property {boolean} inline_cc_form Whether stripe cc should use
+ * inline cc
+ * form or separate inputs.
+ * @property {{[k:string]:CreditCardIcon}} icons Contains supported cc icons.
+ * @property {boolean} showSavedCards Used to indicate whether saved cards
+ * can be used.
+ * @property {boolean} showSaveOption Used to indicate whether the option to
+ * save card can be displayed.
+ * @property {Object} supports List of features supported by the payment gateway
+ */
+/* eslint-enable jsdoc/valid-types */
+
+/**
+ * @typedef {Object} StripeElementOptions
+ *
+ * @property {Object} options The configuration object for stripe
+ * elements.
+ * @property {function(boolean)} onActive A callback for setting whether an
+ * element is active or not. "Active"
+ * means it's not empty.
+ * @property {string} error Any error message from the stripe
+ * element.
+ * @property {function(string)} setError A callback for setting an error
+ * message.
+ */
+
+export {};
diff --git a/client/blocks/stripe-utils/utils.js b/client/blocks/stripe-utils/utils.js
new file mode 100644
index 000000000..caa142787
--- /dev/null
+++ b/client/blocks/stripe-utils/utils.js
@@ -0,0 +1,175 @@
+/* global wc_stripe_payment_request_params */
+
+/**
+ * External dependencies
+ */
+import { getSetting } from '@woocommerce/settings';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { errorTypes, errorCodes } from './constants';
+
+/**
+ * @typedef {import('./type-defs').StripeServerData} StripeServerData
+ * @typedef {import('./type-defs').StripePaymentItem} StripePaymentItem
+ * @typedef {import('./type-defs').StripePaymentRequest} StripePaymentRequest
+ * @typedef {import('@woocommerce/type-defs/registered-payment-method-props').PreparedCartTotalItem} CartTotalItem
+ */
+
+/**
+ * Stripe data comes form the server passed on a global object.
+ *
+ * @return {StripeServerData} Stripe server data.
+ */
+const getStripeServerData = () => {
+ const stripeServerData = getSetting( 'stripe_data', null );
+ if ( ! stripeServerData ) {
+ throw new Error( 'Stripe initialization data is not available' );
+ }
+ return stripeServerData;
+};
+
+/**
+ * Returns the public api key for the stripe payment method
+ *
+ * @throws Error
+ * @return {string} The public api key for the stripe payment method.
+ */
+const getApiKey = () => {
+ const apiKey = getStripeServerData().publicKey;
+ if ( ! apiKey ) {
+ throw new Error(
+ 'There is no api key available for stripe. Make sure it is available on the wc.stripe_data.stripe.key property.'
+ );
+ }
+ return apiKey;
+};
+
+/**
+ * Creates a payment request using cart data from WooCommerce.
+ *
+ * @param {Object} stripe - The Stripe JS object.
+ * @param {Object} cart - The cart data response from the store's AJAX API.
+ *
+ * @return {Object} A Stripe payment request.
+ */
+export const createPaymentRequestUsingCart = ( stripe, cart ) => {
+ const options = {
+ total: cart.order_data.total,
+ currency: cart.order_data.currency,
+ country: cart.order_data.country_code,
+ requestPayerName: true,
+ requestPayerEmail: true,
+ requestPayerPhone:
+ wc_stripe_payment_request_params.checkout.needs_payer_phone,
+ requestShipping: cart.shipping_required ? true : false,
+ displayItems: cart.order_data.displayItems,
+ };
+
+ // Puerto Rico (PR) is the only US territory/possession that's supported by Stripe.
+ // Since it's considered a US state by Stripe, we need to do some special mapping.
+ if ( options.country === 'PR' ) {
+ options.country = 'US';
+ }
+
+ return stripe.paymentRequest( options );
+};
+
+const isNonFriendlyError = ( type ) =>
+ [
+ errorTypes.INVALID_REQUEST,
+ errorTypes.API_CONNECTION,
+ errorTypes.API_ERROR,
+ errorTypes.AUTHENTICATION_ERROR,
+ errorTypes.RATE_LIMIT_ERROR,
+ ].includes( type );
+
+const getErrorMessageForCode = ( code ) => {
+ const messages = {
+ [ errorCodes.INVALID_NUMBER ]: __(
+ 'The card number is not a valid credit card number.',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.INVALID_EXPIRY_MONTH ]: __(
+ 'The card expiration month is invalid.',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.INVALID_EXPIRY_YEAR ]: __(
+ 'The card expiration year is invalid.',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.INVALID_CVC ]: __(
+ 'The card security code is invalid.',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.INCORRECT_NUMBER ]: __(
+ 'The card number is incorrect.',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.INCOMPLETE_NUMBER ]: __(
+ 'The card number is incomplete.',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.INCOMPLETE_CVC ]: __(
+ 'The card security code is incomplete.',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.INCOMPLETE_EXPIRY ]: __(
+ 'The card expiration date is incomplete.',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.EXPIRED_CARD ]: __(
+ 'The card has expired.',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.INCORRECT_CVC ]: __(
+ 'The card security code is incorrect.',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.INCORRECT_ZIP ]: __(
+ 'The card zip code failed validation.',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.INVALID_EXPIRY_YEAR_PAST ]: __(
+ 'The card expiration year is in the past',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.CARD_DECLINED ]: __(
+ 'The card was declined.',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.MISSING ]: __(
+ 'There is no card on a customer that is being charged.',
+ 'woocommerce-gateway-stripe'
+ ),
+ [ errorCodes.PROCESSING_ERROR ]: __(
+ 'An error occurred while processing the card.',
+ 'woocommerce-gateway-stripe'
+ ),
+ };
+ return messages[ code ] || null;
+};
+
+const getErrorMessageForTypeAndCode = ( type, code = '' ) => {
+ switch ( type ) {
+ case errorTypes.INVALID_EMAIL:
+ return __(
+ 'Invalid email address, please correct and try again.',
+ 'woocommerce-gateway-stripe'
+ );
+ case isNonFriendlyError( type ):
+ return __(
+ 'Unable to process this payment, please try again or use alternative method.',
+ 'woocommerce-gateway-stripe'
+ );
+ case errorTypes.CARD_ERROR:
+ return getErrorMessageForCode( code );
+ case errorTypes.VALIDATION_ERROR:
+ return ''; // These are shown inline.
+ }
+ return null;
+};
+
+export { getStripeServerData, getApiKey, getErrorMessageForTypeAndCode };
diff --git a/client/blocks/three-d-secure/index.js b/client/blocks/three-d-secure/index.js
new file mode 100644
index 000000000..585120031
--- /dev/null
+++ b/client/blocks/three-d-secure/index.js
@@ -0,0 +1,2 @@
+export * from './three-d-secure-payment-handler';
+export * from './use-payment-intents';
diff --git a/client/blocks/three-d-secure/three-d-secure-payment-handler.js b/client/blocks/three-d-secure/three-d-secure-payment-handler.js
new file mode 100644
index 000000000..85bbfc93a
--- /dev/null
+++ b/client/blocks/three-d-secure/three-d-secure-payment-handler.js
@@ -0,0 +1,31 @@
+/**
+ * External dependencies
+ */
+import { Elements, useStripe } from '@stripe/react-stripe-js';
+
+/**
+ * Internal dependencies
+ */
+import { usePaymentIntents } from './use-payment-intents';
+
+const sourceIdNoop = () => void null;
+
+const Handler = ( { eventRegistration, emitResponse } ) => {
+ const stripe = useStripe();
+ const { onCheckoutAfterProcessingWithSuccess } = eventRegistration;
+ usePaymentIntents(
+ stripe,
+ onCheckoutAfterProcessingWithSuccess,
+ sourceIdNoop,
+ emitResponse
+ );
+ return null;
+};
+
+export const ThreeDSecurePaymentHandler = ( { stripe, ...props } ) => {
+ return (
+
+
+
+ );
+};
diff --git a/client/blocks/three-d-secure/use-payment-intents.js b/client/blocks/three-d-secure/use-payment-intents.js
new file mode 100644
index 000000000..f1e80d834
--- /dev/null
+++ b/client/blocks/three-d-secure/use-payment-intents.js
@@ -0,0 +1,104 @@
+/**
+ * External dependencies
+ */
+import { useEffect } from '@wordpress/element';
+
+/**
+ * @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EmitResponseProps} EmitResponseProps
+ * @typedef {import('../stripe-utils/type-defs').Stripe} Stripe
+ */
+
+/**
+ * Opens the modal for PaymentIntent authorizations.
+ *
+ * @param {Object} params Params object.
+ * @param {Stripe} params.stripe The stripe object.
+ * @param {Object} params.paymentDetails The payment details from the
+ * server after checkout processing.
+ * @param {string} params.errorContext Context where errors will be added.
+ * @param {string} params.errorType Type of error responses.
+ * @param {string} params.successType Type of success responses.
+ */
+const openIntentModal = ( {
+ stripe,
+ paymentDetails,
+ errorContext,
+ errorType,
+ successType,
+} ) => {
+ const checkoutResponse = { type: successType };
+ if (
+ ! paymentDetails.setup_intent &&
+ ! paymentDetails.payment_intent_secret
+ ) {
+ return true;
+ }
+ const isSetupIntent = !! paymentDetails.setupIntent;
+ const verificationUrl = paymentDetails.verification_endpoint;
+ const intentSecret = isSetupIntent
+ ? paymentDetails.setup_intent
+ : paymentDetails.payment_intent_secret;
+ return stripe[ isSetupIntent ? 'confirmCardSetup' : 'confirmCardPayment' ](
+ intentSecret
+ )
+ .then( function ( response ) {
+ if ( response.error ) {
+ throw response.error;
+ }
+ const intent =
+ response[ isSetupIntent ? 'setupIntent' : 'paymentIntent' ];
+ if (
+ intent.status !== 'requires_capture' &&
+ intent.status !== 'succeeded'
+ ) {
+ return checkoutResponse;
+ }
+ checkoutResponse.redirectUrl = verificationUrl;
+ return checkoutResponse;
+ } )
+ .catch( function ( error ) {
+ checkoutResponse.type = errorType;
+ checkoutResponse.message = error.message;
+ checkoutResponse.retry = true;
+ checkoutResponse.messageContext = errorContext;
+ // Reports back to the server.
+ window.fetch( verificationUrl + '&is_ajax' );
+ return checkoutResponse;
+ } );
+};
+
+export const usePaymentIntents = (
+ stripe,
+ subscriber,
+ setSourceId,
+ emitResponse
+) => {
+ useEffect( () => {
+ const unsubscribe = subscriber( async ( { processingResponse } ) => {
+ const paymentDetails = processingResponse.paymentDetails || {};
+ const response = await openIntentModal( {
+ stripe,
+ paymentDetails,
+ errorContext: emitResponse.noticeContexts.PAYMENTS,
+ errorType: emitResponse.responseTypes.ERROR,
+ successType: emitResponse.responseTypes.SUCCESS,
+ } );
+ if (
+ response.type === emitResponse.responseTypes.ERROR &&
+ response.retry
+ ) {
+ setSourceId( '' );
+ }
+
+ return response;
+ } );
+ return () => unsubscribe();
+ }, [
+ subscriber,
+ emitResponse.noticeContexts.PAYMENTS,
+ emitResponse.responseTypes.ERROR,
+ emitResponse.responseTypes.SUCCESS,
+ setSourceId,
+ stripe,
+ ] );
+};
diff --git a/includes/class-wc-gateway-stripe.php b/includes/class-wc-gateway-stripe.php
index 7c94cd35f..8afec35c2 100644
--- a/includes/class-wc-gateway-stripe.php
+++ b/includes/class-wc-gateway-stripe.php
@@ -442,6 +442,7 @@ public function payment_scripts() {
]
);
+ $stripe_params['stripe_locale'] = WC_Stripe_Helper::convert_wc_locale_to_stripe_locale( get_locale() );
$stripe_params['no_prepaid_card_msg'] = __( 'Sorry, we\'re not accepting prepaid cards at this time. Your credit card has not been charged. Please try with alternative payment method.', 'woocommerce-gateway-stripe' );
$stripe_params['no_sepa_owner_msg'] = __( 'Please enter your IBAN account name.', 'woocommerce-gateway-stripe' );
$stripe_params['no_sepa_iban_msg'] = __( 'Please enter your IBAN account number.', 'woocommerce-gateway-stripe' );
diff --git a/includes/class-wc-stripe-blocks-support.php b/includes/class-wc-stripe-blocks-support.php
new file mode 100644
index 000000000..ffe56b208
--- /dev/null
+++ b/includes/class-wc-stripe-blocks-support.php
@@ -0,0 +1,335 @@
+settings = get_option( 'woocommerce_stripe_settings', [] );
+ }
+
+ /**
+ * Returns if this payment method should be active. If false, the scripts will not be enqueued.
+ *
+ * @return boolean
+ */
+ public function is_active() {
+ return ! empty( $this->settings['enabled'] ) && 'yes' === $this->settings['enabled'];
+ }
+
+ /**
+ * Returns an array of scripts/handles to be registered for this payment method.
+ *
+ * @return array
+ */
+ public function get_payment_method_script_handles() {
+ $asset_path = WC_STRIPE_PLUGIN_PATH . '/build/index.asset.php';
+ $version = WC_STRIPE_VERSION;
+ $dependencies = [];
+ if ( file_exists( $asset_path ) ) {
+ $asset = require $asset_path;
+ $version = is_array( $asset ) && isset( $asset['version'] )
+ ? $asset['version']
+ : $version;
+ $dependencies = is_array( $asset ) && isset( $asset['dependencies'] )
+ ? $asset['dependencies']
+ : $dependencies;
+ }
+ wp_register_script(
+ 'wc-stripe-blocks-integration',
+ WC_STRIPE_PLUGIN_URL . '/build/index.js',
+ $dependencies,
+ $version,
+ true
+ );
+ wp_set_script_translations(
+ 'wc-stripe-blocks-integration',
+ 'woocommerce-gateway-stripe'
+ );
+
+ return [ 'wc-stripe-blocks-integration' ];
+ }
+
+ /**
+ * Returns an array of key=>value pairs of data made available to the payment methods script.
+ *
+ * @return array
+ */
+ public function get_payment_method_data() {
+ return [
+ 'stripeTotalLabel' => $this->get_total_label(),
+ 'publicKey' => $this->get_publishable_key(),
+ 'allowPrepaidCard' => $this->get_allow_prepaid_card(),
+ 'title' => $this->get_title(),
+ 'button' => [
+ 'type' => $this->get_button_type(),
+ 'theme' => $this->get_button_theme(),
+ 'height' => $this->get_button_height(),
+ 'locale' => $this->get_button_locale(),
+ 'customLabel' => isset( $this->settings['payment_request_button_label'] ) ? $this->settings['payment_request_button_label'] : 'Buy now',
+ ],
+ 'inline_cc_form' => $this->get_inline_cc_form(),
+ 'icons' => $this->get_icons(),
+ 'showSavedCards' => $this->get_show_saved_cards(),
+ 'showSaveOption' => $this->get_show_save_option(),
+ 'supports' => $this->get_supported_features(),
+ ];
+ }
+
+ /**
+ * Determine if store allows cards to be saved during checkout.
+ *
+ * @return bool True if merchant allows shopper to save card (payment method) during checkout.
+ */
+ private function get_show_saved_cards() {
+ return isset( $this->settings['saved_cards'] ) ? 'yes' === $this->settings['saved_cards'] : false;
+ }
+
+ /**
+ * Determine if the checkbox to enable the user to save their payment method should be shown.
+ *
+ * @return bool True if the save payment checkbox should be displayed to the user.
+ */
+ private function get_show_save_option() {
+ $saved_cards = $this->get_show_saved_cards();
+ // This assumes that Stripe supports `tokenization` - currently this is true, based on
+ // https://github.com/woocommerce/woocommerce-gateway-stripe/blob/master/includes/class-wc-gateway-stripe.php#L95 .
+ // See https://github.com/woocommerce/woocommerce-gateway-stripe/blob/ad19168b63df86176cbe35c3e95203a245687640/includes/class-wc-gateway-stripe.php#L271 and
+ // https://github.com/woocommerce/woocommerce/wiki/Payment-Token-API .
+ return apply_filters( 'wc_stripe_display_save_payment_method_checkbox', filter_var( $saved_cards, FILTER_VALIDATE_BOOLEAN ) );
+ }
+
+ /**
+ * Returns the label to use accompanying the total in the stripe statement.
+ *
+ * @return string Statement descriptor.
+ */
+ private function get_total_label() {
+ return ! empty( $this->settings['statement_descriptor'] ) ? WC_Stripe_Helper::clean_statement_descriptor( $this->settings['statement_descriptor'] ) : '';
+ }
+
+ /**
+ * Returns the publishable api key for the Stripe service.
+ *
+ * @return string Public api key.
+ */
+ private function get_publishable_key() {
+ $test_mode = ( ! empty( $this->settings['testmode'] ) && 'yes' === $this->settings['testmode'] );
+ $setting_key = $test_mode ? 'test_publishable_key' : 'publishable_key';
+ return ! empty( $this->settings[ $setting_key ] ) ? $this->settings[ $setting_key ] : '';
+ }
+
+ /**
+ * Returns whether to allow prepaid cards for payments.
+ *
+ * @return bool True means to allow prepaid card (default).
+ */
+ private function get_allow_prepaid_card() {
+ return apply_filters( 'wc_stripe_allow_prepaid_card', true );
+ }
+
+ /**
+ * Returns the title string to use in the UI (customisable via admin settings screen).
+ *
+ * @return string Title / label string
+ */
+ private function get_title() {
+ return isset( $this->settings['title'] ) ? $this->settings['title'] : __( 'Credit / Debit Card', 'woocommerce-gateway-stripe' );
+ }
+
+ /**
+ * Return the button type for the payment button.
+ *
+ * @return string Defaults to 'default'.
+ */
+ private function get_button_type() {
+ return isset( $this->settings['payment_request_button_type'] ) ? $this->settings['payment_request_button_type'] : 'default';
+ }
+
+ /**
+ * Return the theme to use for the payment button.
+ *
+ * @return string Defaults to 'dark'.
+ */
+ private function get_button_theme() {
+ return isset( $this->settings['payment_request_button_theme'] ) ? $this->settings['payment_request_button_theme'] : 'dark';
+ }
+
+ /**
+ * Return the height for the payment button.
+ *
+ * @return string A pixel value for the height (defaults to '64').
+ */
+ private function get_button_height() {
+ return isset( $this->settings['payment_request_button_height'] ) ? str_replace( 'px', '', $this->settings['payment_request_button_height'] ) : '64';
+ }
+
+ /**
+ * Return the inline cc option.
+ *
+ * @return boolean True if the inline CC form option is enabled.
+ */
+ private function get_inline_cc_form() {
+ return isset( $this->settings['inline_cc_form'] ) && 'yes' === $this->settings['inline_cc_form'];
+ }
+
+ /**
+ * Return the locale for the payment button.
+ *
+ * @return string Defaults to en_US.
+ */
+ private function get_button_locale() {
+ return apply_filters( 'wc_stripe_payment_request_button_locale', substr( get_locale(), 0, 2 ) );
+ }
+
+ /**
+ * Return the icons urls.
+ *
+ * @return array Arrays of icons metadata.
+ */
+ private function get_icons() {
+ $icons_src = [
+ 'visa' => [
+ 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/visa.svg',
+ 'alt' => __( 'Visa', 'woocommerce-gateway-stripe' ),
+ ],
+ 'amex' => [
+ 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/amex.svg',
+ 'alt' => __( 'American Express', 'woocommerce-gateway-stripe' ),
+ ],
+ 'mastercard' => [
+ 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/mastercard.svg',
+ 'alt' => __( 'Mastercard', 'woocommerce-gateway-stripe' ),
+ ],
+ ];
+
+ if ( 'USD' === get_woocommerce_currency() ) {
+ $icons_src['discover'] = [
+ 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/discover.svg',
+ 'alt' => __( 'Discover', 'woocommerce-gateway-stripe' ),
+ ];
+ $icons_src['jcb'] = [
+ 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/jcb.svg',
+ 'alt' => __( 'JCB', 'woocommerce-gateway-stripe' ),
+ ];
+ $icons_src['diners'] = [
+ 'src' => WC_STRIPE_PLUGIN_URL . '/assets/images/diners.svg',
+ 'alt' => __( 'Diners', 'woocommerce-gateway-stripe' ),
+ ];
+ }
+ return $icons_src;
+ }
+
+ /**
+ * Add payment request data to the order meta as hooked on the
+ * woocommerce_rest_checkout_process_payment_with_context action.
+ *
+ * @param PaymentContext $context Holds context for the payment.
+ * @param PaymentResult $result Result object for the payment.
+ */
+ public function add_payment_request_order_meta( PaymentContext $context, PaymentResult &$result ) {
+ $data = $context->payment_data;
+ if ( ! empty( $data['payment_request_type'] ) && 'stripe' === $context->payment_method ) {
+ $this->add_order_meta( $context->order, $data['payment_request_type'] );
+ }
+
+ // hook into stripe error processing so that we can capture the error to
+ // payment details (which is added to notices and thus not helpful for
+ // this context).
+ if ( 'stripe' === $context->payment_method ) {
+ add_action(
+ 'wc_gateway_stripe_process_payment_error',
+ function( $error ) use ( &$result ) {
+ $payment_details = $result->payment_details;
+ $payment_details['errorMessage'] = wp_strip_all_tags( $error->getLocalizedMessage() );
+ $result->set_payment_details( $payment_details );
+ }
+ );
+ }
+ }
+
+ /**
+ * Handles any potential stripe intents on the order that need handled.
+ *
+ * This is configured to execute after legacy payment processing has
+ * happened on the woocommerce_rest_checkout_process_payment_with_context
+ * action hook.
+ *
+ * @param PaymentContext $context Holds context for the payment.
+ * @param PaymentResult $result Result object for the payment.
+ */
+ public function add_stripe_intents( PaymentContext $context, PaymentResult &$result ) {
+ if ( 'stripe' === $context->payment_method
+ && (
+ ! empty( $result->payment_details['payment_intent_secret'] )
+ || ! empty( $result->payment_details['setup_intent_secret'] )
+ )
+ ) {
+ $payment_details = $result->payment_details;
+ $payment_details['verification_endpoint'] = add_query_arg(
+ [
+ 'order' => $context->order->get_id(),
+ 'nonce' => wp_create_nonce( 'wc_stripe_confirm_pi' ),
+ 'redirect_to' => rawurlencode( $result->redirect_url ),
+ ],
+ home_url() . \WC_Ajax::get_endpoint( 'wc_stripe_verify_intent' )
+ );
+ $result->set_payment_details( $payment_details );
+ $result->set_status( 'success' );
+ }
+ }
+
+ /**
+ * Handles adding information about the payment request type used to the order meta.
+ *
+ * @param \WC_Order $order The order being processed.
+ * @param string $payment_request_type The payment request type used for payment.
+ */
+ private function add_order_meta( \WC_Order $order, $payment_request_type ) {
+ if ( 'apple_pay' === $payment_request_type ) {
+ $order->set_payment_method_title( 'Apple Pay (Stripe)' );
+ $order->save();
+ } elseif ( 'google_pay' === $payment_request_type ) {
+ $order->set_payment_method_title( 'Google Pay (Stripe)' );
+ $order->save();
+ } elseif ( 'payment_request_api' === $payment_request_type ) {
+ $order->set_payment_method_title( 'Payment Request (Stripe)' );
+ $order->save();
+ }
+ }
+
+ /**
+ * Returns an array of supported features.
+ *
+ * @return string[]
+ */
+ public function get_supported_features() {
+ $gateway = new WC_Gateway_Stripe();
+ return array_filter( $gateway->supports, [ $gateway, 'supports' ] );
+ }
+}
diff --git a/includes/class-wc-stripe-helper.php b/includes/class-wc-stripe-helper.php
index 593d5cbd5..ffe334981 100644
--- a/includes/class-wc-stripe-helper.php
+++ b/includes/class-wc-stripe-helper.php
@@ -510,4 +510,91 @@ public static function clean_statement_descriptor( $statement_descriptor = '' )
return $statement_descriptor;
}
+
+ /**
+ * Converts a WooCommerce locale to the closest supported by Stripe.js.
+ *
+ * Stripe.js supports only a subset of IETF language tags, if a country specific locale is not supported we use
+ * the default for that language (https://stripe.com/docs/js/appendix/supported_locales).
+ * If no match is found we return 'auto' so Stripe.js uses the browser locale.
+ *
+ * @param string $wc_locale The locale to convert.
+ *
+ * @return string Closest locale supported by Stripe ('auto' if NONE).
+ */
+ public static function convert_wc_locale_to_stripe_locale( $wc_locale ) {
+ // List copied from: https://stripe.com/docs/js/appendix/supported_locales.
+ $supported = [
+ 'ar', // Arabic.
+ 'bg', // Bulgarian (Bulgaria).
+ 'cs', // Czech (Czech Republic).
+ 'da', // Danish.
+ 'de', // German (Germany).
+ 'el', // Greek (Greece).
+ 'en', // English.
+ 'en-GB', // English (United Kingdom).
+ 'es', // Spanish (Spain).
+ 'es-419', // Spanish (Latin America).
+ 'et', // Estonian (Estonia).
+ 'fi', // Finnish (Finland).
+ 'fr', // French (France).
+ 'fr-CA', // French (Canada).
+ 'he', // Hebrew (Israel).
+ 'hu', // Hungarian (Hungary).
+ 'id', // Indonesian (Indonesia).
+ 'it', // Italian (Italy).
+ 'ja', // Japanese.
+ 'lt', // Lithuanian (Lithuania).
+ 'lv', // Latvian (Latvia).
+ 'ms', // Malay (Malaysia).
+ 'mt', // Maltese (Malta).
+ 'nb', // Norwegian Bokmål.
+ 'nl', // Dutch (Netherlands).
+ 'pl', // Polish (Poland).
+ 'pt-BR', // Portuguese (Brazil).
+ 'pt', // Portuguese (Brazil).
+ 'ro', // Romanian (Romania).
+ 'ru', // Russian (Russia).
+ 'sk', // Slovak (Slovakia).
+ 'sl', // Slovenian (Slovenia).
+ 'sv', // Swedish (Sweden).
+ 'th', // Thai.
+ 'tr', // Turkish (Turkey).
+ 'zh', // Chinese Simplified (China).
+ 'zh-HK', // Chinese Traditional (Hong Kong).
+ 'zh-TW', // Chinese Traditional (Taiwan).
+ ];
+
+ // Stripe uses '-' instead of '_' (used in WordPress).
+ $locale = str_replace( '_', '-', $wc_locale );
+
+ if ( in_array( $locale, $supported, true ) ) {
+ return $locale;
+ }
+
+ // The plugin has been fully translated for Spanish (Ecuador), Spanish (Mexico), and
+ // Spanish(Venezuela), and partially (88% at 2021-05-14) for Spanish (Colombia).
+ // We need to map these locales to Stripe's Spanish (Latin America) 'es-419' locale.
+ // This list should be updated if more localized versions of Latin American Spanish are
+ // made available.
+ $lowercase_locale = strtolower( $wc_locale );
+ $translated_latin_american_locales = [
+ 'es_co', // Spanish (Colombia).
+ 'es_ec', // Spanish (Ecuador).
+ 'es_mx', // Spanish (Mexico).
+ 'es_ve', // Spanish (Venezuela).
+ ];
+ if ( in_array( $lowercase_locale, $translated_latin_american_locales, true ) ) {
+ return 'es-419';
+ }
+
+ // Finally, we check if the "base locale" is available.
+ $base_locale = substr( $wc_locale, 0, 2 );
+ if ( in_array( $base_locale, $supported, true ) ) {
+ return $base_locale;
+ }
+
+ // Default to 'auto' so Stripe.js uses the browser locale.
+ return 'auto';
+ }
}
diff --git a/includes/payment-methods/class-wc-stripe-payment-request.php b/includes/payment-methods/class-wc-stripe-payment-request.php
index 248b66311..f3f7963ca 100644
--- a/includes/payment-methods/class-wc-stripe-payment-request.php
+++ b/includes/payment-methods/class-wc-stripe-payment-request.php
@@ -165,10 +165,11 @@ public static function instance() {
* This is needed so nonces can be verified by AJAX Request.
*
* @since 4.0.0
+ * @version 5.2.0
* @return void
*/
public function set_session() {
- if ( ! is_product() || ( isset( WC()->session ) && WC()->session->has_session() ) ) {
+ if ( ! $this->is_product() || ( isset( WC()->session ) && WC()->session->has_session() ) ) {
return;
}
@@ -317,17 +318,15 @@ public function get_product_price( $product ) {
* Gets the product data for the currently viewed page
*
* @since 4.0.0
- * @version 4.0.0
+ * @version 5.2.0
* @return mixed Returns false if not on a product page, the product information otherwise.
*/
public function get_product_data() {
- if ( ! is_product() ) {
+ if ( ! $this->is_product() ) {
return false;
}
- global $post;
-
- $product = wc_get_product( $post->ID );
+ $product = $this->get_product();
if ( 'variable' === $product->get_type() ) {
$variation_attributes = $product->get_variation_attributes();
@@ -408,12 +407,21 @@ public function filter_gateway_title( $title, $id ) {
$order = wc_get_order( $post->ID );
$method_title = is_object( $order ) ? $order->get_payment_method_title() : '';
- if ( 'stripe' === $id && ! empty( $method_title ) && 'Apple Pay (Stripe)' === $method_title ) {
- return $method_title;
- }
+ if ( 'stripe' === $id && ! empty( $method_title ) ) {
+ if ( 'Apple Pay (Stripe)' === $method_title
+ || 'Google Pay (Stripe)' === $method_title
+ || 'Payment Request (Stripe)' === $method_title
+ ) {
+ return $method_title;
+ }
- if ( 'stripe' === $id && ! empty( $method_title ) && 'Chrome Payment Request (Stripe)' === $method_title ) {
- return $method_title;
+ // We renamed 'Chrome Payment Request' to just 'Payment Request' since Payment Requests
+ // are supported by other browsers besides Chrome. As such, we need to check for the
+ // old title to make sure older orders still reflect that they were paid via Payment
+ // Request Buttons.
+ if ( 'Chrome Payment Request (Stripe)' === $method_title ) {
+ return 'Payment Request (Stripe)';
+ }
}
return $title;
@@ -467,10 +475,11 @@ public function add_order_meta( $order_id, $posted_data ) {
if ( 'apple_pay' === $payment_request_type ) {
$order->set_payment_method_title( 'Apple Pay (Stripe)' );
$order->save();
- }
-
- if ( 'payment_request_api' === $payment_request_type ) {
- $order->set_payment_method_title( 'Chrome Payment Request (Stripe)' );
+ } elseif ( 'google_pay' === $payment_request_type ) {
+ $order->set_payment_method_title( 'Google Pay (Stripe)' );
+ $order->save();
+ } elseif ( 'payment_request_api' === $payment_request_type ) {
+ $order->set_payment_method_title( 'Payment Request (Stripe)' );
$order->save();
}
}
@@ -533,11 +542,56 @@ public function allowed_items_in_cart() {
return true;
}
+ /**
+ * Checks if this page contains a cart or checkout block.
+ *
+ * @since 5.2.0
+ * @return boolean
+ */
+ public function is_block() {
+ return has_block( 'woocommerce/cart' ) || has_block( 'woocommerce/checkout' );
+ }
+
+ /**
+ * Checks if this is a product page or content contains a product_page shortcode.
+ *
+ * @since 5.2.0
+ * @return boolean
+ */
+ public function is_product() {
+ return is_product() || wc_post_content_has_shortcode( 'product_page' );
+ }
+
+ /**
+ * Get product from product page or product_page shortcode.
+ *
+ * @since 5.2.0
+ * @return WC_Product Product object.
+ */
+ public function get_product() {
+ global $post;
+
+ if ( is_product() ) {
+ return wc_get_product( $post->ID );
+ } elseif ( wc_post_content_has_shortcode( 'product_page' ) ) {
+ // Get id from product_page shortcode.
+ preg_match( '/\[product_page id="(?\d+)"\]/', $post->post_content, $shortcode_match );
+
+ if ( ! isset( $shortcode_match['id'] ) ) {
+ return false;
+ }
+
+ return wc_get_product( $shortcode_match['id'] );
+ }
+
+ return false;
+ }
+
/**
* Load public scripts and styles.
*
* @since 3.1.0
- * @version 4.0.0
+ * @version 5.2.0
*/
public function scripts() {
// If keys are not set bail.
@@ -552,11 +606,14 @@ public function scripts() {
return;
}
- if ( ! is_product() && ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) ) {
+ // If page is not supported, bail.
+ if ( ! $this->is_block() && ! $this->is_product() && ! is_cart() && ! is_checkout() && ! isset( $_GET['pay_for_order'] ) ) {
return;
}
- if ( is_product() && ! $this->should_show_payment_button_on_product_page() ) {
+ if ( $this->is_product() && ! $this->should_show_payment_button_on_product_page() ) {
+ return;
+ } elseif ( ! $this->should_show_payment_button_on_cart() ) {
return;
}
@@ -601,7 +658,7 @@ public function scripts() {
'css_selector' => $this->custom_button_selector(),
'branded_type' => $this->get_button_branded_type(),
],
- 'is_product_page' => is_product(),
+ 'is_product_page' => $this->is_product(),
'product' => $this->get_product_data(),
];
@@ -622,7 +679,7 @@ public function scripts() {
* Display the payment request button.
*
* @since 4.0.0
- * @version 4.0.0
+ * @version 5.2.0
*/
public function display_payment_request_button_html() {
global $post;
@@ -633,19 +690,13 @@ public function display_payment_request_button_html() {
return;
}
- if ( ! is_cart() && ! is_checkout() && ! is_product() && ! isset( $_GET['pay_for_order'] ) ) {
+ if ( ! is_cart() && ! is_checkout() && ! $this->is_product() && ! isset( $_GET['pay_for_order'] ) ) {
return;
}
if ( is_checkout() && ! apply_filters( 'wc_stripe_show_payment_request_on_checkout', false, $post ) ) {
return;
}
-
- if ( is_product() && ! $this->should_show_payment_button_on_product_page() ) {
- return;
- } elseif ( ! $this->should_show_payment_button_on_cart() ) {
- return;
- }
?>